diff --git a/.github/agents/agentic-workflows.agent.md b/.github/agents/agentic-workflows.md similarity index 57% rename from .github/agents/agentic-workflows.agent.md rename to .github/agents/agentic-workflows.md index 7ed300e00c..08c6d9a24f 100644 --- a/.github/agents/agentic-workflows.agent.md +++ b/.github/agents/agentic-workflows.md @@ -1,5 +1,6 @@ --- -description: GitHub Agentic Workflows (gh-aw) - Create, debug, and upgrade AI-powered workflows with intelligent prompt routing +name: Agentic Workflows +description: GitHub Agentic Workflows (gh-aw) - Create, debug, and upgrade AI-powered workflows with intelligent prompt routing. disable-model-invocation: true --- @@ -7,18 +8,29 @@ disable-model-invocation: true This agent helps you work with **GitHub Agentic Workflows (gh-aw)**, a CLI extension for creating AI-powered workflows in natural language using markdown files. +## Repository Instructions Overlay + +If `.github/aw/instructions.md` exists, load it with: +@.github/aw/instructions.md + +Precedence: repository overlay instructions override defaults in this agent when they conflict. + ## What This Agent Does This is a **dispatcher agent** that routes your request to the appropriate specialized prompt based on your task: - **Creating new workflows**: Routes to `create` prompt - **Updating existing workflows**: Routes to `update` prompt -- **Debugging workflows**: Routes to `debug` prompt +- **Debugging workflows**: Routes to `debug` prompt - **Upgrading workflows**: Routes to `upgrade-agentic-workflows` prompt - **Creating report-generating workflows**: Routes to `report` prompt — consult this whenever the workflow posts status updates, audits, analyses, or any structured output as issues, discussions, or comments - **Creating shared components**: Routes to `create-shared-agentic-workflow` prompt - **Fixing Dependabot PRs**: Routes to `dependabot` prompt — use this when Dependabot opens PRs that modify generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`). Never merge those PRs directly; instead update the source `.md` files and rerun `gh aw compile --dependabot` to bundle all fixes - **Analyzing test coverage**: Routes to `test-coverage` prompt — consult this whenever the workflow reads, analyzes, or reports on test coverage data from PRs or CI runs +- **Rendering ASCII charts in markdown**: Routes to `asciicharts` guide — consult this whenever the workflow needs compact charts that render reliably in GitHub issues, comments, or discussions +- **CLI commands and triggering workflows**: Routes to `cli-commands` guide — consult this whenever the user asks how to run, compile, debug, or manage workflows from the command line, or when they need the MCP tool equivalent of a `gh aw` command +- **Reducing token consumption / cost optimization**: Routes to `token-optimization` guide — consult this whenever the user asks how to reduce token usage, lower costs, speed up workflows, or measure the impact of prompt changes with experiments +- **Choosing workflow architectures and design patterns**: Routes to `patterns` guide — consult this whenever the user asks for strategy, architecture, operating models, or pattern selection for agentic workflows Workflows may optionally include: @@ -30,7 +42,7 @@ Workflows may optionally include: - Workflow files: `.github/workflows/*.md` and `.github/workflows/**/*.md` - Workflow lock files: `.github/workflows/*.lock.yml` - Shared components: `.github/workflows/shared/*.md` -- Configuration: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/github-agentic-workflows.md +- Configuration: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/github-agentic-workflows.md` ## Problems This Solves @@ -49,30 +61,32 @@ When you interact with this agent, it will: ## Available Prompts +> **Note**: The prompt and reference files listed below are located in the [`github/gh-aw`](https://github.com/github/gh-aw) repository and are **not available locally** in this repository. Load them from their public URLs. + ### Create New Workflow **Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet -**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/create-agentic-workflow.md +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/create-agentic-workflow.md` **Use cases**: - "Create a workflow that triages issues" - "I need a workflow to label pull requests" - "Design a weekly research automation" -### Update Existing Workflow +### Update Existing Workflow **Load when**: User wants to modify, improve, or refactor an existing workflow -**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/update-agentic-workflow.md +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/update-agentic-workflow.md` **Use cases**: - "Add web-fetch tool to the issue-classifier workflow" - "Update the PR reviewer to use discussions instead of issues" - "Improve the prompt for the weekly-research workflow" -### Debug Workflow +### Debug Workflow **Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors -**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/debug-agentic-workflow.md +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/debug-agentic-workflow.md` **Use cases**: - "Why is this workflow failing?" @@ -82,7 +96,7 @@ When you interact with this agent, it will: ### Upgrade Agentic Workflows **Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations -**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/upgrade-agentic-workflows.md +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/upgrade-agentic-workflows.md` **Use cases**: - "Upgrade all workflows to the latest version" @@ -92,7 +106,7 @@ When you interact with this agent, it will: ### Create a Report-Generating Workflow **Load when**: The workflow being created or updated produces reports — recurring status updates, audit summaries, analyses, or any structured output posted as a GitHub issue, discussion, or comment -**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/report.md +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/report.md` **Use cases**: - "Create a weekly CI health report" @@ -102,7 +116,7 @@ When you interact with this agent, it will: ### Create Shared Agentic Workflow **Load when**: User wants to create a reusable workflow component or wrap an MCP server -**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/create-shared-agentic-workflow.md +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/create-shared-agentic-workflow.md` **Use cases**: - "Create a shared component for Notion integration" @@ -112,7 +126,7 @@ When you interact with this agent, it will: ### Fix Dependabot PRs **Load when**: User needs to close or fix open Dependabot PRs that update dependencies in generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`) -**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/dependabot.md +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/dependabot.md` **Use cases**: - "Fix the open Dependabot PRs for npm dependencies" @@ -122,19 +136,54 @@ When you interact with this agent, it will: ### Analyze Test Coverage **Load when**: The workflow reads, analyzes, or reports test coverage — whether triggered by a PR, a schedule, or a slash command. Always consult this prompt before designing the coverage data strategy. -**Prompt file**: https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/test-coverage.md +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/test-coverage.md` **Use cases**: - "Create a workflow that comments coverage on PRs" - "Analyze coverage trends over time" - "Add a coverage gate that blocks PRs below a threshold" +### CLI Commands Reference +**Load when**: The user asks how to run, compile, debug, or manage workflows from the command line; needs the MCP tool equivalent of a `gh aw` command; or is in a restricted environment (e.g., Copilot Cloud) without direct CLI access. + +**Reference file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/cli-commands.md` + +**Use cases**: +- "How do I trigger workflow X on the main branch?" +- "What's the MCP equivalent of `gh aw logs`?" +- "I'm in Copilot Cloud — how do I compile a workflow?" +- "Show me all available gh aw commands" + +### Token Consumption Optimization +**Load when**: The user asks how to reduce token usage, lower workflow costs, make a workflow faster or cheaper, or measure the impact of prompt or configuration changes. + +**Reference file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/token-optimization.md` + +**Use cases**: +- "How do I reduce the token cost of this workflow?" +- "My workflow is too expensive — how do I optimize it?" +- "How do I compare token usage between two runs?" +- "Should I use gh-proxy or the MCP server?" +- "How do I use sub-agents to reduce costs?" +- "How do I measure the impact of a prompt change?" + +### Workflow Pattern Selection +**Load when**: The user asks for architecture, strategy, operating model selection, or pattern recommendations for building agentic workflows. + +**Reference file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/patterns.md` + +**Use cases**: +- "Which pattern should I use for multi-repo rollout?" +- "How should I structure this workflow architecture?" +- "What pattern fits slash-command triage?" +- "Should this be DispatchOps or DailyOps?" + ## Instructions When a user interacts with you: 1. **Identify the task type** from the user's request -2. **Load the appropriate prompt** from the GitHub repository URLs listed above +2. **Load the appropriate prompt** from the URLs listed above 3. **Follow the loaded prompt's instructions** exactly 4. **If uncertain**, ask clarifying questions to determine the right prompt @@ -147,6 +196,10 @@ gh aw init # Generate the lock file for a workflow gh aw compile [workflow-name] +# Trigger a workflow on demand (preferred over gh workflow run) +gh aw run # interactive input collection +gh aw run --ref main # run on a specific branch + # Debug workflow runs gh aw logs [workflow-name] gh aw audit @@ -169,10 +222,12 @@ gh aw compile --validate ## Important Notes -- Always reference the instructions file at https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/github-agentic-workflows.md for complete documentation +- Always reference the instructions file at `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/github-agentic-workflows.md` for complete documentation - Use the MCP tool `agentic-workflows` when running in GitHub Copilot Cloud - Workflows must be compiled to `.lock.yml` files before running in GitHub Actions - **Bash tools are enabled by default** - Don't restrict bash commands unnecessarily since workflows are sandboxed by the AWF - Follow security best practices: minimal permissions, explicit network access, no template injection -- **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See https://github.com/github/gh-aw/blob/v0.64.2/.github/aw/network.md for the full list of valid ecosystem identifiers and domain patterns. +- **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/network.md` for the full list of valid ecosystem identifiers and domain patterns. - **Single-file output**: When creating a workflow, produce exactly **one** workflow `.md` file. Do not create separate documentation files (architecture docs, runbooks, usage guides, etc.). If documentation is needed, add a brief `## Usage` section inside the workflow file itself. +- **Triggering runs**: Always use `gh aw run ` to trigger a workflow on demand — not `gh workflow run .lock.yml`. `gh aw run` handles workflow resolution by short name, input parsing and validation, and correct run-tracking for agentic workflows. Use `--ref ` to run on a specific branch. +- **CLI commands reference**: For a complete guide on all `gh aw` commands and their MCP tool equivalents (for restricted environments), see `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/cli-commands.md` diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 20f2503163..528dbe85c2 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -1,39 +1,34 @@ { "entries": { - "actions/checkout@v6.0.2": { + "actions/checkout@v7": { "repo": "actions/checkout", - "version": "v6.0.2", - "sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd" + "version": "v7", + "sha": "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" }, - "actions/download-artifact@v8.0.0": { + "actions/download-artifact@v8.0.1": { "repo": "actions/download-artifact", - "version": "v8.0.0", - "sha": "70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3" + "version": "v8.0.1", + "sha": "3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" }, - "actions/github-script@v8": { + "actions/github-script@v9": { "repo": "actions/github-script", - "version": "v8", - "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" + "version": "v9", + "sha": "373c709c69115d41ff229c7e5df9f8788daa9553" }, - "actions/github-script@v9.0.0": { - "repo": "actions/github-script", - "version": "v9.0.0", - "sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3" - }, - "actions/upload-artifact@v7.0.0": { + "actions/upload-artifact@v7.0.1": { "repo": "actions/upload-artifact", - "version": "v7.0.0", - "sha": "bbbca2ddaa5d8feaa63e36b76fdaad77386f024f" + "version": "v7.0.1", + "sha": "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" }, - "github/gh-aw-actions/setup@v0.77.5": { - "repo": "github/gh-aw-actions/setup", - "version": "v0.77.5", - "sha": "3ea13c02d765410340d533515cb31a7eef2baaf0" + "github/gh-aw-actions/setup-cli@v0.82.10": { + "repo": "github/gh-aw-actions/setup-cli", + "version": "v0.82.10", + "sha": "05205436a78512d71a2d842e46586ed05f4fa058" }, - "github/gh-aw/actions/setup@v0.52.1": { - "repo": "github/gh-aw/actions/setup", - "version": "v0.52.1", - "sha": "a86e657586e4ac5f549a790628971ec02f6a4a8f" + "github/gh-aw-actions/setup@v0.82.10": { + "repo": "github/gh-aw-actions/setup", + "version": "v0.82.10", + "sha": "05205436a78512d71a2d842e46586ed05f4fa058" } } } diff --git a/.github/instructions/dotnet-e2e.instructions.md b/.github/instructions/dotnet-e2e.instructions.md new file mode 100644 index 0000000000..8dcf7d5330 --- /dev/null +++ b/.github/instructions/dotnet-e2e.instructions.md @@ -0,0 +1,9 @@ +--- +applyTo: "dotnet/test/E2E/**/*.cs" +--- + +# .NET E2E test instructions + +- Create and resume sessions through `E2ETestContext` using `Ctx.CreateSessionAsync` and `Ctx.ResumeSessionAsync`. Do not call these methods directly on `CopilotClient`; the context applies the backend selected by the E2E matrix while preserving providers explicitly configured by the test. +- Create clients with `Ctx.CreateClient` so they receive the harness environment, CLI path, authentication defaults, transport handling, and lifecycle tracking. +- Instantiate `CopilotClient` directly only when client construction, startup, shutdown, or disposal is the behavior under test. Keep cleanup explicit in those tests, and still create any sessions through `Ctx` so they participate in backend coverage. diff --git a/.github/lsp.json b/.github/lsp.json index 7535212849..e58456ac43 100644 --- a/.github/lsp.json +++ b/.github/lsp.json @@ -21,24 +21,6 @@ ".go": "go" }, "rootUri": "go" - }, - "rust-analyzer": { - "command": "rust-analyzer", - "fileExtensions": { - ".rs": "rust" - }, - "initializationOptions": { - "cargo": { - "buildScripts": { - "enable": true - }, - "allFeatures": true - }, - "checkOnSave": true, - "check": { - "command": "clippy" - } - } } } } diff --git a/.github/skills/agentic-workflows/SKILL.md b/.github/skills/agentic-workflows/SKILL.md new file mode 100644 index 0000000000..acec3f146c --- /dev/null +++ b/.github/skills/agentic-workflows/SKILL.md @@ -0,0 +1,94 @@ +--- +name: agentic-workflows +description: Route gh-aw workflow design/create/debug/upgrade requests to the right prompts. +--- + +# Agentic Workflows Router + +Use this skill when a user asks to design, create, update, debug, or upgrade GitHub Agentic Workflows in this repository. + +This skill is a dispatcher: identify the task type, load the matching workflow prompt/skill file, and follow it directly. Keep responses concise and ask a clarifying question if the correct prompt is unclear. + +Repository overlay (optional): +- If `.github/aw/instructions.md` exists, load it with `@.github/aw/instructions.md` after loading the matched prompt/skill. +- Precedence: repository overlay instructions override upstream defaults when they conflict. + +Read only the files you need: +Load these files from `github/gh-aw` (they are not available locally). +- `.github/aw/agentic-chat.md` +- `.github/aw/agentic-workflows-mcp.md` +- `.github/aw/asciicharts.md` +- `.github/aw/campaign.md` +- `.github/aw/charts-trending.md` +- `.github/aw/charts.md` +- `.github/aw/cli-commands.md` +- `.github/aw/configure-agentic-engine.md` +- `.github/aw/context.md` +- `.github/aw/create-agentic-workflow-trigger-details.md` +- `.github/aw/create-agentic-workflow.md` +- `.github/aw/create-shared-agentic-workflow.md` +- `.github/aw/debug-agentic-workflow.md` +- `.github/aw/dependabot.md` +- `.github/aw/deployment-status.md` +- `.github/aw/designer.md` +- `.github/aw/evals.md` +- `.github/aw/experiments.md` +- `.github/aw/github-agentic-workflows.md` +- `.github/aw/github-mcp-server.md` +- `.github/aw/instructions.md` +- `.github/aw/llms.md` +- `.github/aw/loop.md` +- `.github/aw/lsp.md` +- `.github/aw/mcp-clis.md` +- `.github/aw/memory-stateful-patterns.md` +- `.github/aw/memory.md` +- `.github/aw/messages.md` +- `.github/aw/multi-agent-research.md` +- `.github/aw/network.md` +- `.github/aw/optimize-agentic-workflow.md` +- `.github/aw/patterns.md` +- `.github/aw/pr-reviewer.md` +- `.github/aw/report.md` +- `.github/aw/reuse.md` +- `.github/aw/safe-outputs-automation.md` +- `.github/aw/safe-outputs-content.md` +- `.github/aw/safe-outputs-management.md` +- `.github/aw/safe-outputs-runtime.md` +- `.github/aw/safe-outputs.md` +- `.github/aw/serena-tool.md` +- `.github/aw/shared-safe-jobs.md` +- `.github/aw/skills.md` +- `.github/aw/subagents.md` +- `.github/aw/syntax-agentic.md` +- `.github/aw/syntax-core.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.md` +- `.github/aw/triggers.md` +- `.github/aw/update-agentic-workflow.md` +- `.github/aw/upgrade-agentic-workflows.md` +- `.github/aw/visual-regression.md` +- `.github/aw/workflow-constraints.md` +- `.github/aw/workflow-editing.md` +- `.github/aw/workflow-patterns.md` + +After loading the matching workflow prompt or skill, follow it directly: +- Design workflows from scratch via interview: `.github/aw/designer.md` +- Create new workflows: `.github/aw/create-agentic-workflow.md` +- Configure or add declarative engines: `.github/aw/configure-agentic-engine.md` +- Update existing workflows: `.github/aw/update-agentic-workflow.md` +- Debug, audit, or investigate workflows: `.github/aw/debug-agentic-workflow.md` +- Upgrade workflows and fix deprecations: `.github/aw/upgrade-agentic-workflows.md` +- Create shared components or MCP wrappers: `.github/aw/create-shared-agentic-workflow.md` +- Create report-generating workflows: `.github/aw/report.md` +- Fix Dependabot manifest PRs: `.github/aw/dependabot.md` +- Analyze coverage workflows: `.github/aw/test-coverage.md` +- Render compact markdown charts: `.github/aw/asciicharts.md` +- Map CLI commands to MCP usage: `.github/aw/cli-commands.md` +- 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` + +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/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml new file mode 100644 index 0000000000..45d836922f --- /dev/null +++ b/.github/workflows/agentics-maintenance.yml @@ -0,0 +1,633 @@ +# This file was automatically generated by pkg/workflow/maintenance_workflow.go (v0.82.10). 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + with: + version: v0.82.10 + + - 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + with: + version: v0.82.10 + + - 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + with: + version: v0.82.10 + + - 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + with: + version: v0.82.10 + + - 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + 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@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + with: + version: v0.82.10 + + - 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/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 7b6d738671..d25689b3b9 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -76,9 +76,9 @@ jobs: # Install gh-aw extension for advanced GitHub CLI features - name: Install gh-aw extension - uses: github/gh-aw/actions/setup-cli@0feed75a980b06f247abbbf80127f8eb2c19e2c5 # v0.74.8 + uses: github/gh-aw-actions/setup-cli@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: - version: v0.74.4 + version: v0.82.10 # Enable repository pre-commit hooks (Spotless checks for Java source changes) - name: Enable pre-commit hooks diff --git a/.github/workflows/cross-repo-issue-analysis.lock.yml b/.github/workflows/cross-repo-issue-analysis.lock.yml index 697ecc0ee0..dc117400bf 100644 --- a/.github/workflows/cross-repo-issue-analysis.lock.yml +++ b/.github/workflows/cross-repo-issue-analysis.lock.yml @@ -1,20 +1,21 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"97b961391ad56ae223a93f2ff91267fed96ce49805bdd921de7549138893d637","body_hash":"653dfb46c89df98eca22ddfb802149d6ade32e9a7ad40dbdc51bfb6b0ba1c4a3","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} -# 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/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"16de319b1db6be0d409d3055ca8fa9f619f2c0120dad678248a45dce07b880b4","body_hash":"653dfb46c89df98eca22ddfb802149d6ade32e9a7ad40dbdc51bfb6b0ba1c4a3","compiler_version":"v0.82.10","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}} +# 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":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"05205436a78512d71a2d842e46586ed05f4fa058","version":"v0.82.10"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"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.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} +# This file was automatically generated by gh-aw (v0.82.10). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ # | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ +# | | | | (_| | __/ | | | |_| | (__ # \_| |_/\__, |\___|_| |_|\__|_|\___| # __/ | -# _ _ |___/ +# _ _ |___/ # | | | | / _| | # | | | | ___ _ __ _ __| |_| | _____ ____ # | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -32,27 +33,30 @@ # - RUNTIME_TRIAGE_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - 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/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# - github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 -# - ghcr.io/github/gh-aw-mcpg:v0.3.22 -# - ghcr.io/github/github-mcp-server:v1.1.0 -# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 +# - ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 +# - ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 +# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b +# - ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 name: "SDK Runtime Triage" on: issues: types: - - labeled + - labeled workflow_dispatch: inputs: aw_context: @@ -81,14 +85,20 @@ jobs: permissions: actions: read contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: body: ${{ steps.sanitized.outputs.body }} comment_id: "" comment_repo: "" + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + 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 }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + oauth_token_check_failed: ${{ steps.check-oauth-tokens.outputs.oauth_token_check_failed == 'true' }} 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 }} @@ -98,17 +108,18 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -116,16 +127,16 @@ jobs: 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.55" - GH_AW_INFO_AGENT_VERSION: "1.0.55" - GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AGENT_VERSION: "1.0.70" + GH_AW_INFO_CLI_VERSION: "v0.82.10" 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.25.58" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -136,13 +147,59 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-crossrepoissueanalysis-${{ github.run_id }} + restore-keys: agentic-workflow-usage-crossrepoissueanalysis- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Restore daily AIC usage cache (artifact fallback) + id: restore-daily-aic-cache-fallback + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }} + GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }} + 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/restore_aic_usage_cache_fallback.cjs'); + await main(); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "SDK Runtime Triage" + GH_AW_WORKFLOW_ID: "cross-repo-issue-analysis" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_HAS_SLASH_COMMAND: "false" + GH_AW_HAS_LABEL_COMMAND: "false" + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + 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_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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false sparse-checkout: | @@ -151,7 +208,6 @@ jobs: .antigravity .claude .codex - .crush .gemini .opencode .pi @@ -159,8 +215,8 @@ jobs: fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" - name: Check workflow lock file @@ -178,7 +234,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.77.5" + GH_AW_COMPILED_VERSION: "v0.82.10" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -196,6 +252,9 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/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 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -215,20 +274,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_38cbf088966d11e6_EOF' + cat << 'GH_AW_PROMPT_41a978c1ce3777a8_EOF' - GH_AW_PROMPT_38cbf088966d11e6_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_38cbf088966d11e6_EOF' + cat << 'GH_AW_PROMPT_41a978c1ce3777a8_EOF' Tools: create_issue, add_labels(max:3), missing_tool, missing_data, noop - GH_AW_PROMPT_38cbf088966d11e6_EOF + GH_AW_PROMPT_41a978c1ce3777a8_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_38cbf088966d11e6_EOF' + cat << 'GH_AW_PROMPT_41a978c1ce3777a8_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -256,13 +315,13 @@ jobs: - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - - GH_AW_PROMPT_38cbf088966d11e6_EOF + + GH_AW_PROMPT_41a978c1ce3777a8_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_38cbf088966d11e6_EOF' + cat << 'GH_AW_PROMPT_41a978c1ce3777a8_EOF' {{#runtime-import .github/workflows/cross-repo-issue-analysis.md}} - GH_AW_PROMPT_38cbf088966d11e6_EOF + GH_AW_PROMPT_41a978c1ce3777a8_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -300,9 +359,9 @@ jobs: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io, getOctokit); - + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); - + // Call the substitution function return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, @@ -340,7 +399,7 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json @@ -353,9 +412,11 @@ jobs: agent: needs: activation + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write issues: read pull-requests: read env: @@ -364,13 +425,17 @@ jobs: GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: crossrepoissueanalysis outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} + 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' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} @@ -380,10 +445,11 @@ 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 }} + 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@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -392,8 +458,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -404,7 +470,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false - name: Create gh-aw temp directory @@ -413,6 +479,11 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" env: GH_TOKEN: ${{ github.token }} + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - env: GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} RUNTIME_TRIAGE_TOKEN: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} @@ -421,21 +492,14 @@ jobs: - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request || github.event.issue.pull_request + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} @@ -447,14 +511,14 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 env: GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} @@ -462,16 +526,11 @@ jobs: script: | const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: activation - path: /tmp/gh-aw - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -483,15 +542,15 @@ 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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_873b138e05029386_EOF' + 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_873b138e05029386_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_f9dc8569c195ba44_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -513,10 +572,7 @@ jobs: }, "labels": { "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 + "type": "array" }, "repo": { "type": "string", @@ -531,7 +587,8 @@ jobs: "required": true, "type": "string", "sanitize": true, - "maxLength": 65000 + "maxLength": 65000, + "minLength": 20 }, "fields": { "type": "array" @@ -641,62 +698,24 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); await main(); - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - # Mask immediately to prevent timing vulnerabilities - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${API_KEY}" - - PORT=3001 - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" - - name: Start MCP Gateway id: start-mcp-gateway env: + GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }} GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + 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 }} 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 }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" - + # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" - export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_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}" @@ -705,29 +724,24 @@ jobs: mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" export DEBUG="*" - + export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - case "${DOCKER_HOST:-}" in - unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; - /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; - * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; - esac - DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.22' - - mkdir -p /home/runner/.copilot + 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.1' + + 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_fc7eecb5bcf8a5c8_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_d97c92af15acf38e_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.1.0", + "container": "ghcr.io/github/github-mcp-server:v1.5.0", "env": { - "GITHUB_HOST": "\${GITHUB_SERVER_URL}", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_HOST": "${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" }, @@ -739,16 +753,35 @@ jobs: } }, "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + "type": "stdio", + "container": "ghcr.io/github/gh-aw-node", + "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], + "args": ["-w", "\${GITHUB_WORKSPACE}"], + "entrypoint": "sh", + "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"], + "env": { + "DEBUG": "*", + "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", + "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", + "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", + "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", + "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", + "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", + "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}", + "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", + "GITHUB_SHA": "\${GITHUB_SHA}", + "GITHUB_TOKEN": "\${GITHUB_TOKEN}", + "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", + "RUNNER_TEMP": "\${RUNNER_TEMP}" }, "guard-policies": { "write-sink": { "accept": [ "*" - ] + ], + "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} } } } @@ -760,7 +793,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_fc7eecb5bcf8a5c8_EOF + GH_AW_MCP_CONFIG_d97c92af15acf38e_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -813,41 +846,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + GH_AW_CHROOT_BINARIES_SOURCE_PATH="${RUNNER_TEMP}/gh-aw" GH_AW_CHROOT_IDENTITY_HOME="${RUNNER_TEMP}/gh-aw/home" node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs" fi GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-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(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 + # 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(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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + 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_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -862,7 +905,8 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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: Detect agent errors if: always() id: detect-agent-errors @@ -870,17 +914,10 @@ jobs: run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Copy Copilot session state files to logs if: always() continue-on-error: true @@ -904,10 +941,10 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,RUNTIME_TRIAGE_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN,RUNTIME_TRIAGE_TOKEN' SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SECRET_RUNTIME_TRIAGE_TOKEN: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} - name: Append agent step summary if: always() @@ -940,6 +977,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + 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'); @@ -961,16 +999,7 @@ jobs: continue-on-error: true env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_firewall_logs.sh" --rootless - name: Parse token usage for step summary if: always() continue-on-error: true @@ -1031,7 +1060,8 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_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') runs-on: ubuntu-slim permissions: contents: read @@ -1041,6 +1071,8 @@ jobs: group: "gh-aw-conclusion-cross-repo-issue-analysis" cancel-in-progress: false queue: max + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -1049,7 +1081,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1058,8 +1090,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1075,6 +1107,98 @@ jobs: 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 + id: download-safe-outputs-manifest + if: always() + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: safe-outputs-items + 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 + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: usage + path: | + /tmp/gh-aw/usage/aw_info.json + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.json + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/github_rate_limits.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + /tmp/gh-aw/usage/activity/summary.json + if-no-files-found: ignore + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache-conclusion + if: always() + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-crossrepoissueanalysis-${{ github.run_id }} + restore-keys: agentic-workflow-usage-crossrepoissueanalysis- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Write daily AIC usage cache entry + id: write-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + github-token: ${{ github.token }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context); + const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + await main(); + - name: Save daily AIC usage cache + id: save-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-crossrepoissueanalysis-${{ github.run_id }} + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Upload daily AIC usage cache artifact + id: upload-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: aic-usage-cache + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + if-no-files-found: ignore + retention-days: 7 - name: Process no-op messages id: noop uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1086,6 +1210,10 @@ jobs: 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_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_WORKFLOW_ID: "cross-repo-issue-analysis" with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | @@ -1153,23 +1281,30 @@ jobs: GH_AW_WORKFLOW_ID: "cross-repo-issue-analysis" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" GH_AW_ENGINE_ID: "copilot" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "20" - GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | @@ -1182,19 +1317,22 @@ jobs: needs: - activation - agent - if: > - always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + if: always() && needs.agent.result != 'skipped' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1203,8 +1341,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1222,7 +1360,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1231,7 +1369,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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 - name: Check if detection needed id: detection_guard if: always() @@ -1250,12 +1388,13 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.copilot/mcp-config.json" rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + 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." @@ -1293,11 +1432,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1307,39 +1446,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + _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:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + 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_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1353,7 +1504,21 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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() + 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'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1397,6 +1562,8 @@ jobs: pre_activation: if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'runtime triage' runs-on: ubuntu-slim + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} matched_command: '' @@ -1406,15 +1573,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Check team membership for workflow id: check_membership @@ -1440,15 +1607,20 @@ jobs: contents: read issues: write pull-requests: write - timeout-minutes: 15 + timeout-minutes: 45 env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/cross-repo-issue-analysis" GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.55" + GH_AW_ENGINE_VERSION: "1.0.70" + 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" 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" @@ -1464,7 +1636,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1473,8 +1645,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1493,7 +1665,7 @@ jobs: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash - run: | + run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. GH_HOST="${GITHUB_SERVER_URL#https://}" @@ -1525,4 +1697,3 @@ jobs: /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - diff --git a/.github/workflows/cross-repo-issue-analysis.md b/.github/workflows/cross-repo-issue-analysis.md index 1719949881..58e07156b6 100644 --- a/.github/workflows/cross-repo-issue-analysis.md +++ b/.github/workflows/cross-repo-issue-analysis.md @@ -14,6 +14,7 @@ permissions: contents: read issues: read pull-requests: read + copilot-requests: write steps: - name: Clone copilot-agent-runtime env: diff --git a/.github/workflows/dotnet-sdk-tests.yml b/.github/workflows/dotnet-sdk-tests.yml index dcf559228c..ecd5dcd09c 100644 --- a/.github/workflows/dotnet-sdk-tests.yml +++ b/.github/workflows/dotnet-sdk-tests.yml @@ -28,19 +28,35 @@ permissions: jobs: test: - name: ".NET SDK Tests (${{ matrix.os }}, ${{ matrix.transport }})" + name: ".NET SDK Tests (${{ matrix.os }}, ${{ matrix.transport }}, ${{ matrix.backend }})" if: github.event.repository.fork == false env: POWERSHELL_UPDATECHECK: Off + COPILOT_SDK_E2E_BACKEND: ${{ matrix.backend }} + DOTNET_TEST_FILTER: ${{ matrix.test-filter }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] transport: ["default", "inprocess"] - # TODO: Re-enable after fixing in-process sqlite file locking on shutdown on Windows + backend: [capi] + # TODO: Re-enable after fixing in-process sqlite file locking on shutdown on Windows. exclude: - os: windows-latest transport: "inprocess" + include: + - os: ubuntu-latest + transport: inprocess + backend: anthropic-messages + test-filter: "FullyQualifiedName~GitHub.Copilot.Test.E2E&E2EBackend!=SelfConfiguredBackend&E2EBackend!=CapiOnly" + - os: ubuntu-latest + transport: inprocess + backend: openai-responses + test-filter: "FullyQualifiedName~GitHub.Copilot.Test.E2E&E2EBackend!=SelfConfiguredBackend&E2EBackend!=CapiOnly" + - os: ubuntu-latest + transport: inprocess + backend: openai-completions + test-filter: "FullyQualifiedName~GitHub.Copilot.Test.E2E&E2EBackend!=SelfConfiguredBackend&E2EBackend!=CapiOnly" runs-on: ${{ matrix.os }} defaults: run: @@ -92,4 +108,9 @@ jobs: - name: Run .NET SDK tests env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} - run: dotnet test --no-build -v n + run: | + args=(--no-build -v n) + if [[ -n "$DOTNET_TEST_FILTER" ]]; then + args+=(--filter "$DOTNET_TEST_FILTER") + fi + dotnet test "${args[@]}" diff --git a/.github/workflows/go-sdk-tests.yml b/.github/workflows/go-sdk-tests.yml index e262961096..365e60373f 100644 --- a/.github/workflows/go-sdk-tests.yml +++ b/.github/workflows/go-sdk-tests.yml @@ -29,7 +29,7 @@ permissions: jobs: test: - name: "Go SDK Tests" + name: "Go SDK Tests (${{ matrix.os }}, ${{ matrix.transport }})" if: github.event.repository.fork == false env: POWERSHELL_UPDATECHECK: Off @@ -37,6 +37,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] + transport: ["default", "inprocess"] runs-on: ${{ matrix.os }} defaults: run: @@ -78,6 +79,12 @@ jobs: if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" + - name: Select inprocess transport + if: matrix.transport == 'inprocess' + run: | + echo "COPILOT_SDK_DEFAULT_CONNECTION=inprocess" >> "$GITHUB_ENV" + echo "GOFLAGS=-tags=copilot_inprocess" >> "$GITHUB_ENV" + - name: Run Go SDK tests env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} diff --git a/.github/workflows/handle-bug.lock.yml b/.github/workflows/handle-bug.lock.yml index 014ed4b689..4c8844361d 100644 --- a/.github/workflows/handle-bug.lock.yml +++ b/.github/workflows/handle-bug.lock.yml @@ -1,20 +1,21 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a473a22cd67feb7f8f5225639fd989cf71705f78c9fe11c3fc757168e1672b0e","body_hash":"376c982b907760113954510ef1aff70d22dcb172c7bb851b2fa3d82121bdbc1c","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"df4e7ec5b346a28c7de5353cbfb15d329d03eb7092f2ded784ee6602108461e6","body_hash":"376c982b907760113954510ef1aff70d22dcb172c7bb851b2fa3d82121bdbc1c","compiler_version":"v0.82.10","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}} +# 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":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"05205436a78512d71a2d842e46586ed05f4fa058","version":"v0.82.10"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"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.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} +# This file was automatically generated by gh-aw (v0.82.10). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ # | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ +# | | | | (_| | __/ | | | |_| | (__ # \_| |_/\__, |\___|_| |_|\__|_|\___| # __/ | -# _ _ |___/ +# _ _ |___/ # | | | | / _| | # | | | | ___ _ __ _ __| |_| | _____ ____ # | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -31,20 +32,24 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - 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/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# - github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 -# - ghcr.io/github/gh-aw-mcpg:v0.3.22 -# - ghcr.io/github/github-mcp-server:v1.1.0 -# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 +# - ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 +# - ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 +# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b +# - ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 name: "Bug Handler" on: @@ -79,7 +84,7 @@ on: permissions: {} concurrency: - group: "gh-aw-${{ github.workflow }}" + group: "gh-aw-handle-bug-${{ github.run_id }}" run-name: "Bug Handler" @@ -89,14 +94,20 @@ jobs: permissions: actions: read contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: artifact_prefix: ${{ steps.artifact-prefix.outputs.prefix }} comment_id: "" comment_repo: "" + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + 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 }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + oauth_token_check_failed: ${{ steps.check-oauth-tokens.outputs.oauth_token_check_failed == 'true' }} 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 }} @@ -108,15 +119,16 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Resolve host repo for activation checkout @@ -144,16 +156,16 @@ jobs: 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.55" - GH_AW_INFO_AGENT_VERSION: "1.0.55" - GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AGENT_VERSION: "1.0.70" + GH_AW_INFO_CLI_VERSION: "v0.82.10" 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.25.58" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -165,11 +177,57 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-handlebug-${{ github.run_id }} + restore-keys: agentic-workflow-usage-handlebug- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Restore daily AIC usage cache (artifact fallback) + id: restore-daily-aic-cache-fallback + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }} + GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }} + 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/restore_aic_usage_cache_fallback.cjs'); + await main(); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "Bug Handler" + GH_AW_WORKFLOW_ID: "handle-bug" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_HAS_SLASH_COMMAND: "false" + GH_AW_HAS_LABEL_COMMAND: "false" + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + 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_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 if: failure() && steps.resolve-host-repo.outputs.target_repo != github.repository run: | @@ -178,7 +236,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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false repository: ${{ steps.resolve-host-repo.outputs.target_repo }} @@ -189,7 +247,6 @@ jobs: .antigravity .claude .codex - .crush .gemini .opencode .pi @@ -197,8 +254,8 @@ jobs: fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" - name: Check workflow lock file @@ -216,13 +273,16 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.77.5" + GH_AW_COMPILED_VERSION: "v0.82.10" 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/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 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -240,20 +300,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_3df18ed0421fc8c1_EOF' + cat << 'GH_AW_PROMPT_04d69bd6df5739b0_EOF' - GH_AW_PROMPT_3df18ed0421fc8c1_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_3df18ed0421fc8c1_EOF' + cat << 'GH_AW_PROMPT_04d69bd6df5739b0_EOF' Tools: add_comment, add_labels, missing_tool, missing_data, noop - GH_AW_PROMPT_3df18ed0421fc8c1_EOF + GH_AW_PROMPT_04d69bd6df5739b0_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_3df18ed0421fc8c1_EOF' + cat << 'GH_AW_PROMPT_04d69bd6df5739b0_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -281,13 +341,13 @@ jobs: - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - - GH_AW_PROMPT_3df18ed0421fc8c1_EOF + + GH_AW_PROMPT_04d69bd6df5739b0_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_3df18ed0421fc8c1_EOF' + cat << 'GH_AW_PROMPT_04d69bd6df5739b0_EOF' {{#runtime-import .github/workflows/handle-bug.md}} - GH_AW_PROMPT_3df18ed0421fc8c1_EOF + GH_AW_PROMPT_04d69bd6df5739b0_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -319,9 +379,9 @@ jobs: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io, getOctokit); - + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); - + // Call the substitution function return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, @@ -356,7 +416,7 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json @@ -369,9 +429,11 @@ jobs: agent: needs: activation + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write issues: read pull-requests: read concurrency: @@ -383,14 +445,18 @@ jobs: GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: handlebug outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} artifact_prefix: ${{ needs.activation.outputs.artifact_prefix }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} + 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' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} @@ -400,10 +466,11 @@ 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 }} + 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@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -412,8 +479,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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Set runtime paths @@ -425,7 +492,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false - name: Create gh-aw temp directory @@ -434,23 +501,21 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" env: GH_TOKEN: ${{ github.token }} + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}activation + path: /tmp/gh-aw - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request || github.event.issue.pull_request + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -462,11 +527,22 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 --rootless + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + GH_AW_GITHUB_MIN_INTEGRITY: 'none' + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); - name: Parse integrity filter lists id: parse-guard-vars env: @@ -474,16 +550,11 @@ jobs: GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ needs.activation.outputs.artifact_prefix }}activation - path: /tmp/gh-aw - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -495,15 +566,15 @@ 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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_788bfbc2e8cbcb67_EOF' + 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_788bfbc2e8cbcb67_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_18a2d1564ec59c01_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -547,10 +618,7 @@ jobs: }, "labels": { "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 + "type": "array" }, "repo": { "type": "string", @@ -639,60 +707,22 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); await main(); - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - # Mask immediately to prevent timing vulnerabilities - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${API_KEY}" - - PORT=3001 - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" - - name: Start MCP Gateway id: start-mcp-gateway env: + GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }} GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + 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 }} 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" - + # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" - export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_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}" @@ -701,29 +731,24 @@ jobs: mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" export DEBUG="*" - + export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - case "${DOCKER_HOST:-}" in - unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; - /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; - * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; - esac - DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.22' - - mkdir -p /home/runner/.copilot + 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.1' + + 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_5cf2254bdcfe4a71_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_e85305aae15b8db5_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.1.0", + "container": "ghcr.io/github/github-mcp-server:v1.5.0", "env": { - "GITHUB_HOST": "\${GITHUB_SERVER_URL}", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_HOST": "${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" }, @@ -738,16 +763,35 @@ jobs: } }, "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + "type": "stdio", + "container": "ghcr.io/github/gh-aw-node", + "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], + "args": ["-w", "\${GITHUB_WORKSPACE}"], + "entrypoint": "sh", + "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"], + "env": { + "DEBUG": "*", + "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", + "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", + "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", + "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", + "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", + "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", + "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}", + "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", + "GITHUB_SHA": "\${GITHUB_SHA}", + "GITHUB_TOKEN": "\${GITHUB_TOKEN}", + "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", + "RUNNER_TEMP": "\${RUNNER_TEMP}" }, "guard-policies": { "write-sink": { "accept": [ "*" - ] + ], + "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} } } } @@ -759,7 +803,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_5cf2254bdcfe4a71_EOF + GH_AW_MCP_CONFIG_e85305aae15b8db5_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -788,41 +832,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + GH_AW_CHROOT_BINARIES_SOURCE_PATH="${RUNNER_TEMP}/gh-aw" GH_AW_CHROOT_IDENTITY_HOME="${RUNNER_TEMP}/gh-aw/home" node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs" fi GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --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 + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + 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_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -837,7 +891,8 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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: Detect agent errors if: always() id: detect-agent-errors @@ -845,17 +900,10 @@ jobs: run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Copy Copilot session state files to logs if: always() continue-on-error: true @@ -879,8 +927,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -914,6 +961,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + 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'); @@ -935,16 +983,7 @@ jobs: continue-on-error: true env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_firewall_logs.sh" --rootless - name: Parse token usage for step summary if: always() continue-on-error: true @@ -1007,17 +1046,19 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_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') runs-on: ubuntu-slim permissions: contents: read - discussions: write issues: write pull-requests: write concurrency: group: "gh-aw-conclusion-handle-bug-${{ inputs.issue_number }}" cancel-in-progress: false queue: max + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -1026,7 +1067,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1035,8 +1076,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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1053,6 +1094,98 @@ jobs: 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 + 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 + 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 + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}usage + path: | + /tmp/gh-aw/usage/aw_info.json + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.json + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/github_rate_limits.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + /tmp/gh-aw/usage/activity/summary.json + if-no-files-found: ignore + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache-conclusion + if: always() + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-handlebug-${{ github.run_id }} + restore-keys: agentic-workflow-usage-handlebug- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Write daily AIC usage cache entry + id: write-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + github-token: ${{ github.token }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context); + const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + await main(); + - name: Save daily AIC usage cache + id: save-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-handlebug-${{ github.run_id }} + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Upload daily AIC usage cache artifact + id: upload-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: aic-usage-cache + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + if-no-files-found: ignore + retention-days: 7 - name: Process no-op messages id: noop uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1064,6 +1197,10 @@ jobs: 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_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_WORKFLOW_ID: "handle-bug" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1131,23 +1268,30 @@ jobs: GH_AW_WORKFLOW_ID: "handle-bug" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" GH_AW_ENGINE_ID: "copilot" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "20" - GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1160,19 +1304,22 @@ jobs: needs: - activation - agent - if: > - always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + if: always() && needs.agent.result != 'skipped' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1181,8 +1328,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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1201,7 +1348,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1210,7 +1357,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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 - name: Check if detection needed id: detection_guard if: always() @@ -1229,12 +1376,13 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.copilot/mcp-config.json" rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + 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." @@ -1272,11 +1420,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1286,39 +1434,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + _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:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + 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_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1332,7 +1492,21 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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() + 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'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1382,18 +1556,22 @@ jobs: runs-on: ubuntu-slim permissions: contents: read - discussions: write issues: write pull-requests: write - timeout-minutes: 15 + timeout-minutes: 45 env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/handle-bug" GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.55" + GH_AW_ENGINE_VERSION: "1.0.70" + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "handle-bug" 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" @@ -1409,7 +1587,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1418,8 +1596,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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1439,7 +1617,7 @@ jobs: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash - run: | + run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. GH_HOST="${GITHUB_SERVER_URL#https://}" @@ -1471,4 +1649,3 @@ jobs: /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - diff --git a/.github/workflows/handle-bug.md b/.github/workflows/handle-bug.md index 7edb33a4fb..8d426ce5d6 100644 --- a/.github/workflows/handle-bug.md +++ b/.github/workflows/handle-bug.md @@ -16,6 +16,7 @@ permissions: contents: read issues: read pull-requests: read + copilot-requests: write tools: github: toolsets: [default] diff --git a/.github/workflows/handle-documentation.lock.yml b/.github/workflows/handle-documentation.lock.yml index 92a284669b..71d73c337c 100644 --- a/.github/workflows/handle-documentation.lock.yml +++ b/.github/workflows/handle-documentation.lock.yml @@ -1,20 +1,21 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"258058e9a5e3bb707bbcfc9157b7b69f64c06547642da2526a1ff441e3a358dd","body_hash":"81c8287f5691cdc10ae8f60c004bb671d9b4942740d73fcc9646e28fbcd8790e","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6c3b9dc8d0f7b54d44175db209cda34e37ea8c635d01ee0a5cba13675053f6cd","body_hash":"81c8287f5691cdc10ae8f60c004bb671d9b4942740d73fcc9646e28fbcd8790e","compiler_version":"v0.82.10","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}} +# 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":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"05205436a78512d71a2d842e46586ed05f4fa058","version":"v0.82.10"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"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.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} +# This file was automatically generated by gh-aw (v0.82.10). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ # | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ +# | | | | (_| | __/ | | | |_| | (__ # \_| |_/\__, |\___|_| |_|\__|_|\___| # __/ | -# _ _ |___/ +# _ _ |___/ # | | | | / _| | # | | | | ___ _ __ _ __| |_| | _____ ____ # | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -31,20 +32,24 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - 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/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# - github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 -# - ghcr.io/github/gh-aw-mcpg:v0.3.22 -# - ghcr.io/github/github-mcp-server:v1.1.0 -# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 +# - ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 +# - ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 +# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b +# - ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 name: "Documentation Handler" on: @@ -79,7 +84,7 @@ on: permissions: {} concurrency: - group: "gh-aw-${{ github.workflow }}" + group: "gh-aw-handle-documentation-${{ github.run_id }}" run-name: "Documentation Handler" @@ -89,14 +94,20 @@ jobs: permissions: actions: read contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: artifact_prefix: ${{ steps.artifact-prefix.outputs.prefix }} comment_id: "" comment_repo: "" + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + 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 }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + oauth_token_check_failed: ${{ steps.check-oauth-tokens.outputs.oauth_token_check_failed == 'true' }} 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 }} @@ -108,15 +119,16 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Resolve host repo for activation checkout @@ -144,16 +156,16 @@ jobs: 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.55" - GH_AW_INFO_AGENT_VERSION: "1.0.55" - GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AGENT_VERSION: "1.0.70" + GH_AW_INFO_CLI_VERSION: "v0.82.10" 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.25.58" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -165,11 +177,57 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-handledocumentation-${{ github.run_id }} + restore-keys: agentic-workflow-usage-handledocumentation- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Restore daily AIC usage cache (artifact fallback) + id: restore-daily-aic-cache-fallback + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }} + GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }} + 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/restore_aic_usage_cache_fallback.cjs'); + await main(); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "Documentation Handler" + GH_AW_WORKFLOW_ID: "handle-documentation" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_HAS_SLASH_COMMAND: "false" + GH_AW_HAS_LABEL_COMMAND: "false" + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + 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_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 if: failure() && steps.resolve-host-repo.outputs.target_repo != github.repository run: | @@ -178,7 +236,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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false repository: ${{ steps.resolve-host-repo.outputs.target_repo }} @@ -189,7 +247,6 @@ jobs: .antigravity .claude .codex - .crush .gemini .opencode .pi @@ -197,8 +254,8 @@ jobs: fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" - name: Check workflow lock file @@ -216,13 +273,16 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.77.5" + GH_AW_COMPILED_VERSION: "v0.82.10" 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/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 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -240,20 +300,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_c1995fcb77e4eb7d_EOF' + cat << 'GH_AW_PROMPT_b3d8e6ce75517df8_EOF' - GH_AW_PROMPT_c1995fcb77e4eb7d_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_c1995fcb77e4eb7d_EOF' + cat << 'GH_AW_PROMPT_b3d8e6ce75517df8_EOF' Tools: add_comment, add_labels, missing_tool, missing_data, noop - GH_AW_PROMPT_c1995fcb77e4eb7d_EOF + GH_AW_PROMPT_b3d8e6ce75517df8_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_c1995fcb77e4eb7d_EOF' + cat << 'GH_AW_PROMPT_b3d8e6ce75517df8_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -281,13 +341,13 @@ jobs: - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - - GH_AW_PROMPT_c1995fcb77e4eb7d_EOF + + GH_AW_PROMPT_b3d8e6ce75517df8_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_c1995fcb77e4eb7d_EOF' + cat << 'GH_AW_PROMPT_b3d8e6ce75517df8_EOF' {{#runtime-import .github/workflows/handle-documentation.md}} - GH_AW_PROMPT_c1995fcb77e4eb7d_EOF + GH_AW_PROMPT_b3d8e6ce75517df8_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -319,9 +379,9 @@ jobs: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io, getOctokit); - + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); - + // Call the substitution function return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, @@ -356,7 +416,7 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json @@ -369,9 +429,11 @@ jobs: agent: needs: activation + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write issues: read pull-requests: read concurrency: @@ -383,14 +445,18 @@ jobs: GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: handledocumentation outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} artifact_prefix: ${{ needs.activation.outputs.artifact_prefix }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} + 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' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} @@ -400,10 +466,11 @@ 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 }} + 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@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -412,8 +479,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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Set runtime paths @@ -425,7 +492,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false - name: Create gh-aw temp directory @@ -434,23 +501,21 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" env: GH_TOKEN: ${{ github.token }} + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}activation + path: /tmp/gh-aw - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request || github.event.issue.pull_request + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -462,11 +527,22 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 --rootless + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + GH_AW_GITHUB_MIN_INTEGRITY: 'none' + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); - name: Parse integrity filter lists id: parse-guard-vars env: @@ -474,16 +550,11 @@ jobs: GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ needs.activation.outputs.artifact_prefix }}activation - path: /tmp/gh-aw - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -495,15 +566,15 @@ 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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_f287fa0f078c345e_EOF' + 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_f287fa0f078c345e_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_6c1b251bac7b1edb_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -547,10 +618,7 @@ jobs: }, "labels": { "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 + "type": "array" }, "repo": { "type": "string", @@ -639,60 +707,22 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); await main(); - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - # Mask immediately to prevent timing vulnerabilities - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${API_KEY}" - - PORT=3001 - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" - - name: Start MCP Gateway id: start-mcp-gateway env: + GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }} GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + 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 }} 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" - + # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" - export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_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}" @@ -701,29 +731,24 @@ jobs: mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" export DEBUG="*" - + export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - case "${DOCKER_HOST:-}" in - unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; - /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; - * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; - esac - DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.22' - - mkdir -p /home/runner/.copilot + 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.1' + + 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_728828b4ea6e4249_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_e85305aae15b8db5_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.1.0", + "container": "ghcr.io/github/github-mcp-server:v1.5.0", "env": { - "GITHUB_HOST": "\${GITHUB_SERVER_URL}", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_HOST": "${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" }, @@ -738,16 +763,35 @@ jobs: } }, "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + "type": "stdio", + "container": "ghcr.io/github/gh-aw-node", + "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], + "args": ["-w", "\${GITHUB_WORKSPACE}"], + "entrypoint": "sh", + "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"], + "env": { + "DEBUG": "*", + "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", + "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", + "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", + "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", + "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", + "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", + "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}", + "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", + "GITHUB_SHA": "\${GITHUB_SHA}", + "GITHUB_TOKEN": "\${GITHUB_TOKEN}", + "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", + "RUNNER_TEMP": "\${RUNNER_TEMP}" }, "guard-policies": { "write-sink": { "accept": [ "*" - ] + ], + "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} } } } @@ -759,7 +803,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_728828b4ea6e4249_EOF + GH_AW_MCP_CONFIG_e85305aae15b8db5_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -788,41 +832,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + GH_AW_CHROOT_BINARIES_SOURCE_PATH="${RUNNER_TEMP}/gh-aw" GH_AW_CHROOT_IDENTITY_HOME="${RUNNER_TEMP}/gh-aw/home" node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs" fi GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --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 + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + 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_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 5 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -837,7 +891,8 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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: Detect agent errors if: always() id: detect-agent-errors @@ -845,17 +900,10 @@ jobs: run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Copy Copilot session state files to logs if: always() continue-on-error: true @@ -879,8 +927,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -914,6 +961,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + 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'); @@ -935,16 +983,7 @@ jobs: continue-on-error: true env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_firewall_logs.sh" --rootless - name: Parse token usage for step summary if: always() continue-on-error: true @@ -1007,17 +1046,19 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_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') runs-on: ubuntu-slim permissions: contents: read - discussions: write issues: write pull-requests: write concurrency: group: "gh-aw-conclusion-handle-documentation-${{ inputs.issue_number }}" cancel-in-progress: false queue: max + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -1026,7 +1067,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1035,8 +1076,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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1053,6 +1094,98 @@ jobs: 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 + 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 + 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 + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}usage + path: | + /tmp/gh-aw/usage/aw_info.json + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.json + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/github_rate_limits.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + /tmp/gh-aw/usage/activity/summary.json + if-no-files-found: ignore + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache-conclusion + if: always() + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-handledocumentation-${{ github.run_id }} + restore-keys: agentic-workflow-usage-handledocumentation- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Write daily AIC usage cache entry + id: write-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + github-token: ${{ github.token }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context); + const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + await main(); + - name: Save daily AIC usage cache + id: save-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-handledocumentation-${{ github.run_id }} + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Upload daily AIC usage cache artifact + id: upload-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: aic-usage-cache + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + if-no-files-found: ignore + retention-days: 7 - name: Process no-op messages id: noop uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1064,6 +1197,10 @@ jobs: 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_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_WORKFLOW_ID: "handle-documentation" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1131,23 +1268,30 @@ jobs: GH_AW_WORKFLOW_ID: "handle-documentation" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" GH_AW_ENGINE_ID: "copilot" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "5" - GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1160,19 +1304,22 @@ jobs: needs: - activation - agent - if: > - always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + if: always() && needs.agent.result != 'skipped' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1181,8 +1328,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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1201,7 +1348,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1210,7 +1357,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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 - name: Check if detection needed id: detection_guard if: always() @@ -1229,12 +1376,13 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.copilot/mcp-config.json" rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + 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." @@ -1272,11 +1420,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1286,39 +1434,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + _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:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + 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_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1332,7 +1492,21 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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() + 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'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1382,18 +1556,22 @@ jobs: runs-on: ubuntu-slim permissions: contents: read - discussions: write issues: write pull-requests: write - timeout-minutes: 15 + timeout-minutes: 45 env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/handle-documentation" GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.55" + GH_AW_ENGINE_VERSION: "1.0.70" + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "handle-documentation" 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" @@ -1409,7 +1587,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1418,8 +1596,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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1439,7 +1617,7 @@ jobs: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash - run: | + run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. GH_HOST="${GITHUB_SERVER_URL#https://}" @@ -1471,4 +1649,3 @@ jobs: /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - diff --git a/.github/workflows/handle-documentation.md b/.github/workflows/handle-documentation.md index 45c21adb1b..12449a85ca 100644 --- a/.github/workflows/handle-documentation.md +++ b/.github/workflows/handle-documentation.md @@ -16,6 +16,7 @@ permissions: contents: read issues: read pull-requests: read + copilot-requests: write tools: github: toolsets: [default] diff --git a/.github/workflows/handle-enhancement.lock.yml b/.github/workflows/handle-enhancement.lock.yml index de163e4e3e..ac4b65a737 100644 --- a/.github/workflows/handle-enhancement.lock.yml +++ b/.github/workflows/handle-enhancement.lock.yml @@ -1,20 +1,21 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"0a1cd53da97b1be36f489e58d1153583dc96c9b436fab3392437a8d498d4d8fb","body_hash":"624219976b9b7078c6bb11c4177925478cfd8316fe8de535a581bdd176eda825","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"f194730258943dec72121a119dba066ff5fee588d79f69fddddd4ca29b56ffd4","body_hash":"624219976b9b7078c6bb11c4177925478cfd8316fe8de535a581bdd176eda825","compiler_version":"v0.82.10","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}} +# 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":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"05205436a78512d71a2d842e46586ed05f4fa058","version":"v0.82.10"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"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.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} +# This file was automatically generated by gh-aw (v0.82.10). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ # | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ +# | | | | (_| | __/ | | | |_| | (__ # \_| |_/\__, |\___|_| |_|\__|_|\___| # __/ | -# _ _ |___/ +# _ _ |___/ # | | | | / _| | # | | | | ___ _ __ _ __| |_| | _____ ____ # | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -31,20 +32,24 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - 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/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# - github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 -# - ghcr.io/github/gh-aw-mcpg:v0.3.22 -# - ghcr.io/github/github-mcp-server:v1.1.0 -# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 +# - ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 +# - ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 +# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b +# - ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 name: "Enhancement Handler" on: @@ -79,7 +84,7 @@ on: permissions: {} concurrency: - group: "gh-aw-${{ github.workflow }}" + group: "gh-aw-handle-enhancement-${{ github.run_id }}" run-name: "Enhancement Handler" @@ -89,14 +94,20 @@ jobs: permissions: actions: read contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: artifact_prefix: ${{ steps.artifact-prefix.outputs.prefix }} comment_id: "" comment_repo: "" + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + 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 }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + oauth_token_check_failed: ${{ steps.check-oauth-tokens.outputs.oauth_token_check_failed == 'true' }} 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 }} @@ -108,15 +119,16 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Resolve host repo for activation checkout @@ -144,16 +156,16 @@ jobs: 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.55" - GH_AW_INFO_AGENT_VERSION: "1.0.55" - GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AGENT_VERSION: "1.0.70" + GH_AW_INFO_CLI_VERSION: "v0.82.10" 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.25.58" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -165,11 +177,57 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-handleenhancement-${{ github.run_id }} + restore-keys: agentic-workflow-usage-handleenhancement- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Restore daily AIC usage cache (artifact fallback) + id: restore-daily-aic-cache-fallback + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }} + GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }} + 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/restore_aic_usage_cache_fallback.cjs'); + await main(); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "Enhancement Handler" + GH_AW_WORKFLOW_ID: "handle-enhancement" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_HAS_SLASH_COMMAND: "false" + GH_AW_HAS_LABEL_COMMAND: "false" + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + 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_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 if: failure() && steps.resolve-host-repo.outputs.target_repo != github.repository run: | @@ -178,7 +236,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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false repository: ${{ steps.resolve-host-repo.outputs.target_repo }} @@ -189,7 +247,6 @@ jobs: .antigravity .claude .codex - .crush .gemini .opencode .pi @@ -197,8 +254,8 @@ jobs: fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" - name: Check workflow lock file @@ -216,13 +273,16 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.77.5" + GH_AW_COMPILED_VERSION: "v0.82.10" 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/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 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -240,20 +300,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_192f9f111edce454_EOF' + cat << 'GH_AW_PROMPT_e59da2f8e25b61b4_EOF' - GH_AW_PROMPT_192f9f111edce454_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_192f9f111edce454_EOF' + cat << 'GH_AW_PROMPT_e59da2f8e25b61b4_EOF' Tools: add_comment, add_labels, missing_tool, missing_data, noop - GH_AW_PROMPT_192f9f111edce454_EOF + GH_AW_PROMPT_e59da2f8e25b61b4_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_192f9f111edce454_EOF' + cat << 'GH_AW_PROMPT_e59da2f8e25b61b4_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -281,13 +341,13 @@ jobs: - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - - GH_AW_PROMPT_192f9f111edce454_EOF + + GH_AW_PROMPT_e59da2f8e25b61b4_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_192f9f111edce454_EOF' + cat << 'GH_AW_PROMPT_e59da2f8e25b61b4_EOF' {{#runtime-import .github/workflows/handle-enhancement.md}} - GH_AW_PROMPT_192f9f111edce454_EOF + GH_AW_PROMPT_e59da2f8e25b61b4_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -319,9 +379,9 @@ jobs: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io, getOctokit); - + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); - + // Call the substitution function return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, @@ -356,7 +416,7 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json @@ -369,9 +429,11 @@ jobs: agent: needs: activation + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write issues: read pull-requests: read concurrency: @@ -383,14 +445,18 @@ jobs: GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: handleenhancement outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} artifact_prefix: ${{ needs.activation.outputs.artifact_prefix }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} + 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' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} @@ -400,10 +466,11 @@ 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 }} + 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@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -412,8 +479,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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Set runtime paths @@ -425,7 +492,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false - name: Create gh-aw temp directory @@ -434,23 +501,21 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" env: GH_TOKEN: ${{ github.token }} + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}activation + path: /tmp/gh-aw - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request || github.event.issue.pull_request + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -462,11 +527,22 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 --rootless + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + GH_AW_GITHUB_MIN_INTEGRITY: 'none' + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); - name: Parse integrity filter lists id: parse-guard-vars env: @@ -474,16 +550,11 @@ jobs: GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ needs.activation.outputs.artifact_prefix }}activation - path: /tmp/gh-aw - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -495,15 +566,15 @@ 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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_7a0b9826ce5c2de6_EOF' + 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_7a0b9826ce5c2de6_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_a36bb21781ffc2fb_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -547,10 +618,7 @@ jobs: }, "labels": { "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 + "type": "array" }, "repo": { "type": "string", @@ -639,60 +707,22 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); await main(); - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - # Mask immediately to prevent timing vulnerabilities - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${API_KEY}" - - PORT=3001 - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" - - name: Start MCP Gateway id: start-mcp-gateway env: + GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }} GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + 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 }} 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" - + # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" - export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_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}" @@ -701,29 +731,24 @@ jobs: mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" export DEBUG="*" - + export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - case "${DOCKER_HOST:-}" in - unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; - /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; - * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; - esac - DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.22' - - mkdir -p /home/runner/.copilot + 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.1' + + 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_fc710c56a8354bbf_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_e85305aae15b8db5_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.1.0", + "container": "ghcr.io/github/github-mcp-server:v1.5.0", "env": { - "GITHUB_HOST": "\${GITHUB_SERVER_URL}", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_HOST": "${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" }, @@ -738,16 +763,35 @@ jobs: } }, "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + "type": "stdio", + "container": "ghcr.io/github/gh-aw-node", + "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], + "args": ["-w", "\${GITHUB_WORKSPACE}"], + "entrypoint": "sh", + "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"], + "env": { + "DEBUG": "*", + "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", + "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", + "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", + "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", + "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", + "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", + "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}", + "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", + "GITHUB_SHA": "\${GITHUB_SHA}", + "GITHUB_TOKEN": "\${GITHUB_TOKEN}", + "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", + "RUNNER_TEMP": "\${RUNNER_TEMP}" }, "guard-policies": { "write-sink": { "accept": [ "*" - ] + ], + "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} } } } @@ -759,7 +803,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_fc710c56a8354bbf_EOF + GH_AW_MCP_CONFIG_e85305aae15b8db5_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -788,41 +832,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + GH_AW_CHROOT_BINARIES_SOURCE_PATH="${RUNNER_TEMP}/gh-aw" GH_AW_CHROOT_IDENTITY_HOME="${RUNNER_TEMP}/gh-aw/home" node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs" fi GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --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 + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + 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_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 5 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -837,7 +891,8 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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: Detect agent errors if: always() id: detect-agent-errors @@ -845,17 +900,10 @@ jobs: run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Copy Copilot session state files to logs if: always() continue-on-error: true @@ -879,8 +927,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -914,6 +961,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + 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'); @@ -935,16 +983,7 @@ jobs: continue-on-error: true env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_firewall_logs.sh" --rootless - name: Parse token usage for step summary if: always() continue-on-error: true @@ -1007,17 +1046,19 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_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') runs-on: ubuntu-slim permissions: contents: read - discussions: write issues: write pull-requests: write concurrency: group: "gh-aw-conclusion-handle-enhancement-${{ inputs.issue_number }}" cancel-in-progress: false queue: max + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -1026,7 +1067,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1035,8 +1076,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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1053,6 +1094,98 @@ jobs: 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 + 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 + 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 + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}usage + path: | + /tmp/gh-aw/usage/aw_info.json + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.json + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/github_rate_limits.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + /tmp/gh-aw/usage/activity/summary.json + if-no-files-found: ignore + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache-conclusion + if: always() + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-handleenhancement-${{ github.run_id }} + restore-keys: agentic-workflow-usage-handleenhancement- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Write daily AIC usage cache entry + id: write-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + github-token: ${{ github.token }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context); + const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + await main(); + - name: Save daily AIC usage cache + id: save-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-handleenhancement-${{ github.run_id }} + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Upload daily AIC usage cache artifact + id: upload-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: aic-usage-cache + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + if-no-files-found: ignore + retention-days: 7 - name: Process no-op messages id: noop uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1064,6 +1197,10 @@ jobs: 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_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_WORKFLOW_ID: "handle-enhancement" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1131,23 +1268,30 @@ jobs: GH_AW_WORKFLOW_ID: "handle-enhancement" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" GH_AW_ENGINE_ID: "copilot" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "5" - GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1160,19 +1304,22 @@ jobs: needs: - activation - agent - if: > - always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + if: always() && needs.agent.result != 'skipped' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1181,8 +1328,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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1201,7 +1348,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1210,7 +1357,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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 - name: Check if detection needed id: detection_guard if: always() @@ -1229,12 +1376,13 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.copilot/mcp-config.json" rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + 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." @@ -1272,11 +1420,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1286,39 +1434,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + _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:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + 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_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1332,7 +1492,21 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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() + 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'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1382,18 +1556,22 @@ jobs: runs-on: ubuntu-slim permissions: contents: read - discussions: write issues: write pull-requests: write - timeout-minutes: 15 + timeout-minutes: 45 env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/handle-enhancement" GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.55" + GH_AW_ENGINE_VERSION: "1.0.70" + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "handle-enhancement" 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" @@ -1409,7 +1587,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1418,8 +1596,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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1439,7 +1617,7 @@ jobs: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash - run: | + run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. GH_HOST="${GITHUB_SERVER_URL#https://}" @@ -1471,4 +1649,3 @@ jobs: /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - diff --git a/.github/workflows/handle-enhancement.md b/.github/workflows/handle-enhancement.md index 6dcb2aa0fd..9043c181c1 100644 --- a/.github/workflows/handle-enhancement.md +++ b/.github/workflows/handle-enhancement.md @@ -16,6 +16,7 @@ permissions: contents: read issues: read pull-requests: read + copilot-requests: write tools: github: toolsets: [default] diff --git a/.github/workflows/handle-question.lock.yml b/.github/workflows/handle-question.lock.yml index 14f7fe1995..5c2a4b849b 100644 --- a/.github/workflows/handle-question.lock.yml +++ b/.github/workflows/handle-question.lock.yml @@ -1,20 +1,21 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"fb6cc48845814496ea0da474d3030f9e02e7d38b5bb346b70ca525c06c271cb1","body_hash":"1bdd19aae2095beb6e3fcf7af755cd102d424de3a8727ef6e4674815950c7e8b","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"92158ba4f7e373cb65457b060c7645569b32c756c13c025837491f6809cf694f","body_hash":"1bdd19aae2095beb6e3fcf7af755cd102d424de3a8727ef6e4674815950c7e8b","compiler_version":"v0.82.10","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}} +# 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":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"05205436a78512d71a2d842e46586ed05f4fa058","version":"v0.82.10"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"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.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} +# This file was automatically generated by gh-aw (v0.82.10). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ # | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ +# | | | | (_| | __/ | | | |_| | (__ # \_| |_/\__, |\___|_| |_|\__|_|\___| # __/ | -# _ _ |___/ +# _ _ |___/ # | | | | / _| | # | | | | ___ _ __ _ __| |_| | _____ ____ # | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -31,20 +32,24 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - 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/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# - github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 -# - ghcr.io/github/gh-aw-mcpg:v0.3.22 -# - ghcr.io/github/github-mcp-server:v1.1.0 -# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 +# - ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 +# - ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 +# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b +# - ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 name: "Question Handler" on: @@ -79,7 +84,7 @@ on: permissions: {} concurrency: - group: "gh-aw-${{ github.workflow }}" + group: "gh-aw-handle-question-${{ github.run_id }}" run-name: "Question Handler" @@ -89,14 +94,20 @@ jobs: permissions: actions: read contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: artifact_prefix: ${{ steps.artifact-prefix.outputs.prefix }} comment_id: "" comment_repo: "" + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + 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 }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + oauth_token_check_failed: ${{ steps.check-oauth-tokens.outputs.oauth_token_check_failed == 'true' }} 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 }} @@ -108,15 +119,16 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Resolve host repo for activation checkout @@ -144,16 +156,16 @@ jobs: 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.55" - GH_AW_INFO_AGENT_VERSION: "1.0.55" - GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AGENT_VERSION: "1.0.70" + GH_AW_INFO_CLI_VERSION: "v0.82.10" 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.25.58" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -165,11 +177,57 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-handlequestion-${{ github.run_id }} + restore-keys: agentic-workflow-usage-handlequestion- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Restore daily AIC usage cache (artifact fallback) + id: restore-daily-aic-cache-fallback + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }} + GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }} + 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/restore_aic_usage_cache_fallback.cjs'); + await main(); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "Question Handler" + GH_AW_WORKFLOW_ID: "handle-question" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_HAS_SLASH_COMMAND: "false" + GH_AW_HAS_LABEL_COMMAND: "false" + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + 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_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 if: failure() && steps.resolve-host-repo.outputs.target_repo != github.repository run: | @@ -178,7 +236,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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false repository: ${{ steps.resolve-host-repo.outputs.target_repo }} @@ -189,7 +247,6 @@ jobs: .antigravity .claude .codex - .crush .gemini .opencode .pi @@ -197,8 +254,8 @@ jobs: fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" - name: Check workflow lock file @@ -216,13 +273,16 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.77.5" + GH_AW_COMPILED_VERSION: "v0.82.10" 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/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 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -240,20 +300,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_0e4131663d1691aa_EOF' + cat << 'GH_AW_PROMPT_a6cfd5b92b97c528_EOF' - GH_AW_PROMPT_0e4131663d1691aa_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_0e4131663d1691aa_EOF' + cat << 'GH_AW_PROMPT_a6cfd5b92b97c528_EOF' Tools: add_comment, add_labels, missing_tool, missing_data, noop - GH_AW_PROMPT_0e4131663d1691aa_EOF + GH_AW_PROMPT_a6cfd5b92b97c528_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_0e4131663d1691aa_EOF' + cat << 'GH_AW_PROMPT_a6cfd5b92b97c528_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -281,13 +341,13 @@ jobs: - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - - GH_AW_PROMPT_0e4131663d1691aa_EOF + + GH_AW_PROMPT_a6cfd5b92b97c528_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_0e4131663d1691aa_EOF' + cat << 'GH_AW_PROMPT_a6cfd5b92b97c528_EOF' {{#runtime-import .github/workflows/handle-question.md}} - GH_AW_PROMPT_0e4131663d1691aa_EOF + GH_AW_PROMPT_a6cfd5b92b97c528_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -319,9 +379,9 @@ jobs: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io, getOctokit); - + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); - + // Call the substitution function return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, @@ -356,7 +416,7 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json @@ -369,9 +429,11 @@ jobs: agent: needs: activation + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write issues: read pull-requests: read concurrency: @@ -383,14 +445,18 @@ jobs: GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: handlequestion outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} artifact_prefix: ${{ needs.activation.outputs.artifact_prefix }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} + 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' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} @@ -400,10 +466,11 @@ 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 }} + 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@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -412,8 +479,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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Set runtime paths @@ -425,7 +492,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false - name: Create gh-aw temp directory @@ -434,23 +501,21 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" env: GH_TOKEN: ${{ github.token }} + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}activation + path: /tmp/gh-aw - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request || github.event.issue.pull_request + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -462,11 +527,22 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 --rootless + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + GH_AW_GITHUB_MIN_INTEGRITY: 'none' + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); - name: Parse integrity filter lists id: parse-guard-vars env: @@ -474,16 +550,11 @@ jobs: GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ needs.activation.outputs.artifact_prefix }}activation - path: /tmp/gh-aw - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -495,15 +566,15 @@ 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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_f18ff0beb4e2bc07_EOF' + 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_f18ff0beb4e2bc07_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_564a988b74c338ef_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -547,10 +618,7 @@ jobs: }, "labels": { "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 + "type": "array" }, "repo": { "type": "string", @@ -639,60 +707,22 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); await main(); - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - # Mask immediately to prevent timing vulnerabilities - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${API_KEY}" - - PORT=3001 - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" - - name: Start MCP Gateway id: start-mcp-gateway env: + GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }} GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + 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 }} 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" - + # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" - export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_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}" @@ -701,29 +731,24 @@ jobs: mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" export DEBUG="*" - + export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - case "${DOCKER_HOST:-}" in - unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; - /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; - * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; - esac - DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.22' - - mkdir -p /home/runner/.copilot + 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.1' + + 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_878c9f46d6eeb406_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_e85305aae15b8db5_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.1.0", + "container": "ghcr.io/github/github-mcp-server:v1.5.0", "env": { - "GITHUB_HOST": "\${GITHUB_SERVER_URL}", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_HOST": "${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" }, @@ -738,16 +763,35 @@ jobs: } }, "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + "type": "stdio", + "container": "ghcr.io/github/gh-aw-node", + "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], + "args": ["-w", "\${GITHUB_WORKSPACE}"], + "entrypoint": "sh", + "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"], + "env": { + "DEBUG": "*", + "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", + "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", + "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", + "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", + "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", + "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", + "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}", + "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", + "GITHUB_SHA": "\${GITHUB_SHA}", + "GITHUB_TOKEN": "\${GITHUB_TOKEN}", + "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", + "RUNNER_TEMP": "\${RUNNER_TEMP}" }, "guard-policies": { "write-sink": { "accept": [ "*" - ] + ], + "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} } } } @@ -759,7 +803,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_878c9f46d6eeb406_EOF + GH_AW_MCP_CONFIG_e85305aae15b8db5_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -788,41 +832,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + GH_AW_CHROOT_BINARIES_SOURCE_PATH="${RUNNER_TEMP}/gh-aw" GH_AW_CHROOT_IDENTITY_HOME="${RUNNER_TEMP}/gh-aw/home" node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs" fi GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --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 + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + 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_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 5 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -837,7 +891,8 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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: Detect agent errors if: always() id: detect-agent-errors @@ -845,17 +900,10 @@ jobs: run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Copy Copilot session state files to logs if: always() continue-on-error: true @@ -879,8 +927,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -914,6 +961,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + 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'); @@ -935,16 +983,7 @@ jobs: continue-on-error: true env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_firewall_logs.sh" --rootless - name: Parse token usage for step summary if: always() continue-on-error: true @@ -1007,17 +1046,19 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_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') runs-on: ubuntu-slim permissions: contents: read - discussions: write issues: write pull-requests: write concurrency: group: "gh-aw-conclusion-handle-question-${{ inputs.issue_number }}" cancel-in-progress: false queue: max + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -1026,7 +1067,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1035,8 +1076,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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1053,6 +1094,98 @@ jobs: 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 + 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 + 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 + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}usage + path: | + /tmp/gh-aw/usage/aw_info.json + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.json + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/github_rate_limits.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + /tmp/gh-aw/usage/activity/summary.json + if-no-files-found: ignore + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache-conclusion + if: always() + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-handlequestion-${{ github.run_id }} + restore-keys: agentic-workflow-usage-handlequestion- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Write daily AIC usage cache entry + id: write-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + github-token: ${{ github.token }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context); + const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + await main(); + - name: Save daily AIC usage cache + id: save-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-handlequestion-${{ github.run_id }} + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Upload daily AIC usage cache artifact + id: upload-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: aic-usage-cache + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + if-no-files-found: ignore + retention-days: 7 - name: Process no-op messages id: noop uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1064,6 +1197,10 @@ jobs: 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_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_WORKFLOW_ID: "handle-question" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1131,23 +1268,30 @@ jobs: GH_AW_WORKFLOW_ID: "handle-question" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" GH_AW_ENGINE_ID: "copilot" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "5" - GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1160,19 +1304,22 @@ jobs: needs: - activation - agent - if: > - always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + if: always() && needs.agent.result != 'skipped' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1181,8 +1328,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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1201,7 +1348,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1210,7 +1357,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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 - name: Check if detection needed id: detection_guard if: always() @@ -1229,12 +1376,13 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.copilot/mcp-config.json" rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + 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." @@ -1272,11 +1420,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1286,39 +1434,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + _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:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + 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_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1332,7 +1492,21 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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() + 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'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1382,18 +1556,22 @@ jobs: runs-on: ubuntu-slim permissions: contents: read - discussions: write issues: write pull-requests: write - timeout-minutes: 15 + timeout-minutes: 45 env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/handle-question" GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.55" + GH_AW_ENGINE_VERSION: "1.0.70" + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "handle-question" 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" @@ -1409,7 +1587,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1418,8 +1596,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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1439,7 +1617,7 @@ jobs: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash - run: | + run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. GH_HOST="${GITHUB_SERVER_URL#https://}" @@ -1471,4 +1649,3 @@ jobs: /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - diff --git a/.github/workflows/handle-question.md b/.github/workflows/handle-question.md index 2bf3a65235..21a10d468a 100644 --- a/.github/workflows/handle-question.md +++ b/.github/workflows/handle-question.md @@ -16,6 +16,7 @@ permissions: contents: read issues: read pull-requests: read + copilot-requests: write tools: github: toolsets: [default] diff --git a/.github/workflows/issue-classification.lock.yml b/.github/workflows/issue-classification.lock.yml index 0fd6359144..e06af36319 100644 --- a/.github/workflows/issue-classification.lock.yml +++ b/.github/workflows/issue-classification.lock.yml @@ -1,20 +1,21 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"1c9f9a62a510a7796b96187fbe0537fd05da1c082d8fab86cd7b99bf001aee01","body_hash":"8e7ac9b7bb6ab07630a10a4a016108ba59f70feadf82a7391ca0ba5504e14bff","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"797f7487a67c2fa4465cb3fd31e17c9f0620bb232b2a4fb693c62cb76d5d5a36","body_hash":"8e7ac9b7bb6ab07630a10a4a016108ba59f70feadf82a7391ca0ba5504e14bff","compiler_version":"v0.82.10","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}} +# 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":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"05205436a78512d71a2d842e46586ed05f4fa058","version":"v0.82.10"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"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.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} +# This file was automatically generated by gh-aw (v0.82.10). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ # | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ +# | | | | (_| | __/ | | | |_| | (__ # \_| |_/\__, |\___|_| |_|\__|_|\___| # __/ | -# _ _ |___/ +# _ _ |___/ # | | | | / _| | # | | | | ___ _ __ _ __| |_| | _____ ____ # | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -31,26 +32,30 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - 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/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# - github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 -# - ghcr.io/github/gh-aw-mcpg:v0.3.22 -# - ghcr.io/github/github-mcp-server:v1.1.0 -# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 +# - ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 +# - ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 +# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b +# - ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 name: "Issue Classification Agent" on: issues: types: - - opened + - opened # roles: all # Roles processed as role check in pre-activation job workflow_dispatch: inputs: @@ -77,14 +82,20 @@ jobs: permissions: actions: read contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: body: ${{ steps.sanitized.outputs.body }} comment_id: "" comment_repo: "" + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + 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 }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + oauth_token_check_failed: ${{ steps.check-oauth-tokens.outputs.oauth_token_check_failed == 'true' }} 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 }} @@ -94,15 +105,16 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -110,16 +122,16 @@ jobs: 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.55" - GH_AW_INFO_AGENT_VERSION: "1.0.55" - GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AGENT_VERSION: "1.0.70" + GH_AW_INFO_CLI_VERSION: "v0.82.10" 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.25.58" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -130,13 +142,59 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-issueclassification-${{ github.run_id }} + restore-keys: agentic-workflow-usage-issueclassification- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Restore daily AIC usage cache (artifact fallback) + id: restore-daily-aic-cache-fallback + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }} + GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }} + 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/restore_aic_usage_cache_fallback.cjs'); + await main(); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "Issue Classification Agent" + GH_AW_WORKFLOW_ID: "issue-classification" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_HAS_SLASH_COMMAND: "false" + GH_AW_HAS_LABEL_COMMAND: "false" + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + 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_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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false sparse-checkout: | @@ -145,7 +203,6 @@ jobs: .antigravity .claude .codex - .crush .gemini .opencode .pi @@ -153,8 +210,8 @@ jobs: fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" - name: Check workflow lock file @@ -172,7 +229,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.77.5" + GH_AW_COMPILED_VERSION: "v0.82.10" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -190,6 +247,9 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/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 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -209,20 +269,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_0e5e0cb2acba7dc0_EOF' + cat << 'GH_AW_PROMPT_2b9644ded951c90e_EOF' - GH_AW_PROMPT_0e5e0cb2acba7dc0_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_0e5e0cb2acba7dc0_EOF' + cat << 'GH_AW_PROMPT_2b9644ded951c90e_EOF' Tools: add_comment, call_workflow, missing_tool, missing_data, noop - GH_AW_PROMPT_0e5e0cb2acba7dc0_EOF + GH_AW_PROMPT_2b9644ded951c90e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_0e5e0cb2acba7dc0_EOF' + cat << 'GH_AW_PROMPT_2b9644ded951c90e_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -250,13 +310,13 @@ jobs: - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - - GH_AW_PROMPT_0e5e0cb2acba7dc0_EOF + + GH_AW_PROMPT_2b9644ded951c90e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_0e5e0cb2acba7dc0_EOF' + cat << 'GH_AW_PROMPT_2b9644ded951c90e_EOF' {{#runtime-import .github/workflows/issue-classification.md}} - GH_AW_PROMPT_0e5e0cb2acba7dc0_EOF + GH_AW_PROMPT_2b9644ded951c90e_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -293,9 +353,9 @@ jobs: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io, getOctokit); - + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); - + // Call the substitution function return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, @@ -332,7 +392,7 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json @@ -345,9 +405,11 @@ jobs: agent: needs: activation + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write issues: read pull-requests: read env: @@ -356,13 +418,17 @@ jobs: GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: issueclassification outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} + 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' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} @@ -372,10 +438,11 @@ 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 }} + 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@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -384,8 +451,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -396,7 +463,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false - name: Create gh-aw temp directory @@ -405,23 +472,21 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" env: GH_TOKEN: ${{ github.token }} + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request || github.event.issue.pull_request + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -433,11 +498,22 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 --rootless + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + GH_AW_GITHUB_MIN_INTEGRITY: 'none' + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); - name: Parse integrity filter lists id: parse-guard-vars env: @@ -445,16 +521,11 @@ jobs: GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: activation - path: /tmp/gh-aw - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -466,15 +537,15 @@ 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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_0e1d49da13fc6a56_EOF' + 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_0e1d49da13fc6a56_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_3e3303377640f8c6_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -699,60 +770,22 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); await main(); - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - # Mask immediately to prevent timing vulnerabilities - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${API_KEY}" - - PORT=3001 - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" - - name: Start MCP Gateway id: start-mcp-gateway env: + GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }} GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + 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 }} 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" - + # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" - export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_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}" @@ -761,29 +794,24 @@ jobs: mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" export DEBUG="*" - + export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - case "${DOCKER_HOST:-}" in - unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; - /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; - * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; - esac - DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.22' - - mkdir -p /home/runner/.copilot + 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.1' + + 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_5ad084c2b5bc2d53_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_e85305aae15b8db5_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.1.0", + "container": "ghcr.io/github/github-mcp-server:v1.5.0", "env": { - "GITHUB_HOST": "\${GITHUB_SERVER_URL}", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_HOST": "${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" }, @@ -798,16 +826,35 @@ jobs: } }, "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + "type": "stdio", + "container": "ghcr.io/github/gh-aw-node", + "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], + "args": ["-w", "\${GITHUB_WORKSPACE}"], + "entrypoint": "sh", + "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"], + "env": { + "DEBUG": "*", + "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", + "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", + "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", + "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", + "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", + "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", + "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}", + "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", + "GITHUB_SHA": "\${GITHUB_SHA}", + "GITHUB_TOKEN": "\${GITHUB_TOKEN}", + "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", + "RUNNER_TEMP": "\${RUNNER_TEMP}" }, "guard-policies": { "write-sink": { "accept": [ "*" - ] + ], + "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} } } } @@ -819,7 +866,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_5ad084c2b5bc2d53_EOF + GH_AW_MCP_CONFIG_e85305aae15b8db5_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -848,41 +895,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + GH_AW_CHROOT_BINARIES_SOURCE_PATH="${RUNNER_TEMP}/gh-aw" GH_AW_CHROOT_IDENTITY_HOME="${RUNNER_TEMP}/gh-aw/home" node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs" fi GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --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 + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + 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_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -897,7 +954,8 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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: Detect agent errors if: always() id: detect-agent-errors @@ -905,17 +963,10 @@ jobs: run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Copy Copilot session state files to logs if: always() continue-on-error: true @@ -939,8 +990,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -974,6 +1024,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + 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'); @@ -995,16 +1046,7 @@ jobs: continue-on-error: true env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_firewall_logs.sh" --rootless - name: Parse token usage for step summary if: always() continue-on-error: true @@ -1062,10 +1104,12 @@ jobs: call-handle-bug: needs: safe_outputs if: needs.safe_outputs.outputs.call_workflow_name == 'handle-bug' + # Imported from called workflow "handle-bug" because GitHub requires the caller job to grant permissions requested by reusable workflow jobs. + # Review the called workflow's job-level permissions in ./.github/workflows/handle-bug.lock.yml. permissions: actions: read contents: read - discussions: write + copilot-requests: write issues: write pull-requests: write uses: ./.github/workflows/handle-bug.lock.yml @@ -1081,10 +1125,12 @@ jobs: call-handle-documentation: needs: safe_outputs if: needs.safe_outputs.outputs.call_workflow_name == 'handle-documentation' + # Imported from called workflow "handle-documentation" because GitHub requires the caller job to grant permissions requested by reusable workflow jobs. + # Review the called workflow's job-level permissions in ./.github/workflows/handle-documentation.lock.yml. permissions: actions: read contents: read - discussions: write + copilot-requests: write issues: write pull-requests: write uses: ./.github/workflows/handle-documentation.lock.yml @@ -1100,10 +1146,12 @@ jobs: call-handle-enhancement: needs: safe_outputs if: needs.safe_outputs.outputs.call_workflow_name == 'handle-enhancement' + # Imported from called workflow "handle-enhancement" because GitHub requires the caller job to grant permissions requested by reusable workflow jobs. + # Review the called workflow's job-level permissions in ./.github/workflows/handle-enhancement.lock.yml. permissions: actions: read contents: read - discussions: write + copilot-requests: write issues: write pull-requests: write uses: ./.github/workflows/handle-enhancement.lock.yml @@ -1119,10 +1167,12 @@ jobs: call-handle-question: needs: safe_outputs if: needs.safe_outputs.outputs.call_workflow_name == 'handle-question' + # Imported from called workflow "handle-question" because GitHub requires the caller job to grant permissions requested by reusable workflow jobs. + # Review the called workflow's job-level permissions in ./.github/workflows/handle-question.lock.yml. permissions: actions: read contents: read - discussions: write + copilot-requests: write issues: write pull-requests: write uses: ./.github/workflows/handle-question.lock.yml @@ -1147,17 +1197,19 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_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') runs-on: ubuntu-slim permissions: contents: read - discussions: write issues: write pull-requests: write concurrency: group: "gh-aw-conclusion-issue-classification" cancel-in-progress: false queue: max + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -1166,7 +1218,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1175,8 +1227,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1192,6 +1244,98 @@ jobs: 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 + id: download-safe-outputs-manifest + if: always() + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: safe-outputs-items + 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 + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: usage + path: | + /tmp/gh-aw/usage/aw_info.json + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.json + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/github_rate_limits.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + /tmp/gh-aw/usage/activity/summary.json + if-no-files-found: ignore + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache-conclusion + if: always() + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-issueclassification-${{ github.run_id }} + restore-keys: agentic-workflow-usage-issueclassification- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Write daily AIC usage cache entry + id: write-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + github-token: ${{ github.token }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context); + const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + await main(); + - name: Save daily AIC usage cache + id: save-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-issueclassification-${{ github.run_id }} + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Upload daily AIC usage cache artifact + id: upload-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: aic-usage-cache + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + if-no-files-found: ignore + retention-days: 7 - name: Process no-op messages id: noop uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1203,6 +1347,10 @@ jobs: 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_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_WORKFLOW_ID: "issue-classification" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1270,23 +1418,30 @@ jobs: GH_AW_WORKFLOW_ID: "issue-classification" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" GH_AW_ENGINE_ID: "copilot" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "10" - GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1299,19 +1454,22 @@ jobs: needs: - activation - agent - if: > - always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + if: always() && needs.agent.result != 'skipped' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1320,8 +1478,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1339,7 +1497,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1348,7 +1506,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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 - name: Check if detection needed id: detection_guard if: always() @@ -1367,12 +1525,13 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.copilot/mcp-config.json" rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + 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." @@ -1410,11 +1569,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1424,39 +1583,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + _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:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + 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_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1470,7 +1641,21 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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() + 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'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1520,18 +1705,22 @@ jobs: runs-on: ubuntu-slim permissions: contents: read - discussions: write issues: write pull-requests: write - timeout-minutes: 15 + timeout-minutes: 45 env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/issue-classification" GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.55" + GH_AW_ENGINE_VERSION: "1.0.70" + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "issue-classification" 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" @@ -1549,7 +1738,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1558,8 +1747,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1578,7 +1767,7 @@ jobs: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash - run: | + run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. GH_HOST="${GITHUB_SERVER_URL#https://}" @@ -1610,4 +1799,3 @@ jobs: /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - diff --git a/.github/workflows/issue-classification.md b/.github/workflows/issue-classification.md index af682461f5..b1e3345f91 100644 --- a/.github/workflows/issue-classification.md +++ b/.github/workflows/issue-classification.md @@ -14,6 +14,7 @@ permissions: contents: read issues: read pull-requests: read + copilot-requests: write tools: github: toolsets: [default] diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml index dce3e9171d..4f4ef28f1a 100644 --- a/.github/workflows/issue-triage.lock.yml +++ b/.github/workflows/issue-triage.lock.yml @@ -1,20 +1,21 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"4472ef96371b3dbbd8e7b52b2612d552047d519ba61344a9b2a92e663fee87ed","body_hash":"30994be7c5c23b102c12a56a325ac313e413a2507dff11d0dc695899379bfbd0","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"596392856be0229eb9fd3a16e2de146d2c9f5f8484809a28e50c861f386652af","body_hash":"30994be7c5c23b102c12a56a325ac313e413a2507dff11d0dc695899379bfbd0","compiler_version":"v0.82.10","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}} +# 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":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"05205436a78512d71a2d842e46586ed05f4fa058","version":"v0.82.10"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"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.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} +# This file was automatically generated by gh-aw (v0.82.10). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ # | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ +# | | | | (_| | __/ | | | |_| | (__ # \_| |_/\__, |\___|_| |_|\__|_|\___| # __/ | -# _ _ |___/ +# _ _ |___/ # | | | | / _| | # | | | | ___ _ __ _ __| |_| | _____ ____ # | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -31,27 +32,30 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - 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/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# - github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 -# - ghcr.io/github/gh-aw-mcpg:v0.3.22 -# - ghcr.io/github/github-mcp-server:v1.1.0 -# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 +# - ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 +# - ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 +# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b +# - ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 name: "Issue Triage Agent" on: issues: types: - - opened + - opened # roles: all # Roles processed as role check in pre-activation job workflow_dispatch: inputs: @@ -78,14 +82,20 @@ jobs: permissions: actions: read contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: body: ${{ steps.sanitized.outputs.body }} comment_id: "" comment_repo: "" + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + 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 }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + oauth_token_check_failed: ${{ steps.check-oauth-tokens.outputs.oauth_token_check_failed == 'true' }} 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 }} @@ -95,15 +105,16 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -111,16 +122,16 @@ jobs: 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.55" - GH_AW_INFO_AGENT_VERSION: "1.0.55" - GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AGENT_VERSION: "1.0.70" + GH_AW_INFO_CLI_VERSION: "v0.82.10" 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.25.58" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -131,13 +142,59 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-issuetriage-${{ github.run_id }} + restore-keys: agentic-workflow-usage-issuetriage- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Restore daily AIC usage cache (artifact fallback) + id: restore-daily-aic-cache-fallback + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }} + GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }} + 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/restore_aic_usage_cache_fallback.cjs'); + await main(); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "Issue Triage Agent" + GH_AW_WORKFLOW_ID: "issue-triage" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_HAS_SLASH_COMMAND: "false" + GH_AW_HAS_LABEL_COMMAND: "false" + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + 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_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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false sparse-checkout: | @@ -146,7 +203,6 @@ jobs: .antigravity .claude .codex - .crush .gemini .opencode .pi @@ -154,8 +210,8 @@ jobs: fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" - name: Check workflow lock file @@ -173,7 +229,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.77.5" + GH_AW_COMPILED_VERSION: "v0.82.10" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -191,6 +247,9 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/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 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -210,20 +269,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_294c35176923eb24_EOF' + cat << 'GH_AW_PROMPT_39930e94844c6d8f_EOF' - GH_AW_PROMPT_294c35176923eb24_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_294c35176923eb24_EOF' + 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_294c35176923eb24_EOF + GH_AW_PROMPT_39930e94844c6d8f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_294c35176923eb24_EOF' + cat << 'GH_AW_PROMPT_39930e94844c6d8f_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -251,13 +310,13 @@ jobs: - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - - GH_AW_PROMPT_294c35176923eb24_EOF + + GH_AW_PROMPT_39930e94844c6d8f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_294c35176923eb24_EOF' + cat << 'GH_AW_PROMPT_39930e94844c6d8f_EOF' {{#runtime-import .github/workflows/issue-triage.md}} - GH_AW_PROMPT_294c35176923eb24_EOF + GH_AW_PROMPT_39930e94844c6d8f_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -294,9 +353,9 @@ jobs: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io, getOctokit); - + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); - + // Call the substitution function return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, @@ -333,7 +392,7 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json @@ -346,9 +405,11 @@ jobs: agent: needs: activation + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write issues: read pull-requests: read env: @@ -357,13 +418,17 @@ jobs: GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: issuetriage outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} + 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' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} @@ -373,10 +438,11 @@ 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 }} + 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@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -385,8 +451,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -397,7 +463,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false - name: Create gh-aw temp directory @@ -406,23 +472,21 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" env: GH_TOKEN: ${{ github.token }} + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request || github.event.issue.pull_request + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -434,14 +498,14 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 env: GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} @@ -449,16 +513,11 @@ jobs: script: | const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: activation - path: /tmp/gh-aw - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -470,15 +529,15 @@ 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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_90ffae57d01667f3_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_bd9befd4ae64abbb_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"],"max":10,"target":"triggering"},"close_issue":{"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_90ffae57d01667f3_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_bd9befd4ae64abbb_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -524,10 +583,7 @@ jobs: }, "labels": { "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 + "type": "array" }, "repo": { "type": "string", @@ -643,10 +699,7 @@ jobs: "issueOrPRNumber": true }, "labels": { - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 + "type": "array" }, "milestone": { "optionalPositiveInteger": true @@ -677,7 +730,7 @@ jobs: "maxLength": 128 } }, - "customValidation": "requiresOneOf:status,title,body" + "customValidation": "requiresOneOf:status,title,body,labels,assignees,milestone" } } uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -687,62 +740,24 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); await main(); - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - # Mask immediately to prevent timing vulnerabilities - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${API_KEY}" - - PORT=3001 - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" - - name: Start MCP Gateway id: start-mcp-gateway env: + GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }} GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + 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 }} 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 }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" - + # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" - export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_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}" @@ -751,29 +766,24 @@ jobs: mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" export DEBUG="*" - + export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - case "${DOCKER_HOST:-}" in - unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; - /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; - * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; - esac - DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.22' - - mkdir -p /home/runner/.copilot + 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.1' + + 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_90b7530930d86f98_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_d97c92af15acf38e_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.1.0", + "container": "ghcr.io/github/github-mcp-server:v1.5.0", "env": { - "GITHUB_HOST": "\${GITHUB_SERVER_URL}", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_HOST": "${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" }, @@ -785,16 +795,35 @@ jobs: } }, "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + "type": "stdio", + "container": "ghcr.io/github/gh-aw-node", + "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], + "args": ["-w", "\${GITHUB_WORKSPACE}"], + "entrypoint": "sh", + "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"], + "env": { + "DEBUG": "*", + "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", + "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", + "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", + "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", + "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", + "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", + "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}", + "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", + "GITHUB_SHA": "\${GITHUB_SHA}", + "GITHUB_TOKEN": "\${GITHUB_TOKEN}", + "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", + "RUNNER_TEMP": "\${RUNNER_TEMP}" }, "guard-policies": { "write-sink": { "accept": [ "*" - ] + ], + "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} } } } @@ -806,7 +835,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_90b7530930d86f98_EOF + GH_AW_MCP_CONFIG_d97c92af15acf38e_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -835,41 +864,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + GH_AW_CHROOT_BINARIES_SOURCE_PATH="${RUNNER_TEMP}/gh-aw" GH_AW_CHROOT_IDENTITY_HOME="${RUNNER_TEMP}/gh-aw/home" node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs" fi GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --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 + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + 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_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -884,7 +923,8 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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: Detect agent errors if: always() id: detect-agent-errors @@ -892,17 +932,10 @@ jobs: run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Copy Copilot session state files to logs if: always() continue-on-error: true @@ -926,8 +959,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -961,6 +993,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + 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'); @@ -982,16 +1015,7 @@ jobs: continue-on-error: true env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_firewall_logs.sh" --rootless - name: Parse token usage for step summary if: always() continue-on-error: true @@ -1052,17 +1076,19 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_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') runs-on: ubuntu-slim permissions: contents: read - discussions: write issues: write pull-requests: write concurrency: group: "gh-aw-conclusion-issue-triage" cancel-in-progress: false queue: max + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -1071,7 +1097,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1080,8 +1106,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1097,6 +1123,98 @@ jobs: 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 + id: download-safe-outputs-manifest + if: always() + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: safe-outputs-items + 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 + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: usage + path: | + /tmp/gh-aw/usage/aw_info.json + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.json + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/github_rate_limits.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + /tmp/gh-aw/usage/activity/summary.json + if-no-files-found: ignore + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache-conclusion + if: always() + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-issuetriage-${{ github.run_id }} + restore-keys: agentic-workflow-usage-issuetriage- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Write daily AIC usage cache entry + id: write-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + github-token: ${{ github.token }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context); + const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + await main(); + - name: Save daily AIC usage cache + id: save-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-issuetriage-${{ github.run_id }} + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Upload daily AIC usage cache artifact + id: upload-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: aic-usage-cache + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + if-no-files-found: ignore + retention-days: 7 - name: Process no-op messages id: noop uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1108,6 +1226,10 @@ jobs: 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_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_WORKFLOW_ID: "issue-triage" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1175,23 +1297,30 @@ jobs: GH_AW_WORKFLOW_ID: "issue-triage" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" GH_AW_ENGINE_ID: "copilot" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "10" - GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1204,19 +1333,22 @@ jobs: needs: - activation - agent - if: > - always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + if: always() && needs.agent.result != 'skipped' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1225,8 +1357,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1244,7 +1376,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1253,7 +1385,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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 - name: Check if detection needed id: detection_guard if: always() @@ -1272,12 +1404,13 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.copilot/mcp-config.json" rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + 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." @@ -1315,11 +1448,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1329,39 +1462,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + 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:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + 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_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1375,7 +1520,21 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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() + 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'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1425,18 +1584,22 @@ jobs: runs-on: ubuntu-slim permissions: contents: read - discussions: write issues: write pull-requests: write - timeout-minutes: 15 + timeout-minutes: 45 env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/issue-triage" GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.55" + GH_AW_ENGINE_VERSION: "1.0.70" + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "issue-triage" 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" @@ -1452,7 +1615,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1461,8 +1624,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1481,7 +1644,7 @@ jobs: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash - run: | + run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. GH_HOST="${GITHUB_SERVER_URL#https://}" @@ -1513,4 +1676,3 @@ jobs: /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - diff --git a/.github/workflows/issue-triage.md b/.github/workflows/issue-triage.md index 72f2042dc9..c4f774c0db 100644 --- a/.github/workflows/issue-triage.md +++ b/.github/workflows/issue-triage.md @@ -14,6 +14,7 @@ permissions: contents: read issues: read pull-requests: read + copilot-requests: write tools: github: toolsets: [default] 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 f099dd9da4..3a7335922b 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,20 +1,21 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"e539700f2389ba9b056bf59e91a04578013218d4a37bbeee4e1db3e88e39af45","body_hash":"41bc10df4ac9179064417476fbb39fef7423d0998dc0beb7bad42e9eb7ab0494","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} -# 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/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a5f19a89f89b0693f86ca89ea90e3a633fe19c17bf4d27214fa9124429cdc156","body_hash":"41bc10df4ac9179064417476fbb39fef7423d0998dc0beb7bad42e9eb7ab0494","compiler_version":"v0.82.10","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}} +# 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":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"05205436a78512d71a2d842e46586ed05f4fa058","version":"v0.82.10"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"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.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} +# This file was automatically generated by gh-aw (v0.82.10). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ # | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ +# | | | | (_| | __/ | | | |_| | (__ # \_| |_/\__, |\___|_| |_|\__|_|\___| # __/ | -# _ _ |___/ +# _ _ |___/ # | | | | / _| | # | | | | ___ _ __ _ __| |_| | _____ ____ # | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -34,21 +35,24 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - 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/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# - github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 -# - ghcr.io/github/gh-aw-mcpg:v0.3.22 -# - ghcr.io/github/github-mcp-server:v1.1.0 -# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 +# - ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 +# - ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 +# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b +# - ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 name: "Java Handwritten Code Adaptation After CLI Upgrade" on: @@ -81,13 +85,19 @@ jobs: permissions: actions: read contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: comment_id: "" comment_repo: "" + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + 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 }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + oauth_token_check_failed: ${{ steps.check-oauth-tokens.outputs.oauth_token_check_failed == 'true' }} 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 }} @@ -95,15 +105,16 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -111,16 +122,16 @@ jobs: 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.55" - GH_AW_INFO_AGENT_VERSION: "1.0.55" - GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AGENT_VERSION: "1.0.70" + GH_AW_INFO_CLI_VERSION: "v0.82.10" 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.25.58" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -131,13 +142,59 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-javaadapthandwrittencodetoacceptupgradechanges-${{ github.run_id }} + restore-keys: agentic-workflow-usage-javaadapthandwrittencodetoacceptupgradechanges- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Restore daily AIC usage cache (artifact fallback) + id: restore-daily-aic-cache-fallback + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }} + GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }} + 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/restore_aic_usage_cache_fallback.cjs'); + await main(); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" + GH_AW_WORKFLOW_ID: "java-adapt-handwritten-code-to-accept-upgrade-changes" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_HAS_SLASH_COMMAND: "false" + GH_AW_HAS_LABEL_COMMAND: "false" + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + 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_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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false sparse-checkout: | @@ -146,7 +203,6 @@ jobs: .antigravity .claude .codex - .crush .gemini .opencode .pi @@ -154,8 +210,8 @@ jobs: fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" - name: Check workflow lock file @@ -173,13 +229,16 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.77.5" + GH_AW_COMPILED_VERSION: "v0.82.10" 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/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 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -198,23 +257,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_c4f6bd591c056d39_EOF' + cat << 'GH_AW_PROMPT_67432b380d9d8ebb_EOF' - GH_AW_PROMPT_c4f6bd591c056d39_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_c4f6bd591c056d39_EOF' + cat << 'GH_AW_PROMPT_67432b380d9d8ebb_EOF' Tools: add_comment(max:10), push_to_pull_request_branch, missing_tool, missing_data, noop - GH_AW_PROMPT_c4f6bd591c056d39_EOF + GH_AW_PROMPT_67432b380d9d8ebb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_c4f6bd591c056d39_EOF' + cat << 'GH_AW_PROMPT_67432b380d9d8ebb_EOF' - GH_AW_PROMPT_c4f6bd591c056d39_EOF + GH_AW_PROMPT_67432b380d9d8ebb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_c4f6bd591c056d39_EOF' + cat << 'GH_AW_PROMPT_67432b380d9d8ebb_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -242,13 +301,13 @@ jobs: - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - - GH_AW_PROMPT_c4f6bd591c056d39_EOF + + GH_AW_PROMPT_67432b380d9d8ebb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_c4f6bd591c056d39_EOF' + cat << 'GH_AW_PROMPT_67432b380d9d8ebb_EOF' {{#runtime-import .github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.md}} - GH_AW_PROMPT_c4f6bd591c056d39_EOF + GH_AW_PROMPT_67432b380d9d8ebb_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -282,9 +341,9 @@ jobs: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io, getOctokit); - + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); - + // Call the substitution function return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, @@ -320,7 +379,7 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json @@ -333,23 +392,29 @@ jobs: agent: needs: activation + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest permissions: actions: read contents: read + copilot-requests: write 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_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: javaadapthandwrittencodetoacceptupgradechanges outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} + 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' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} @@ -359,10 +424,11 @@ 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 }} + 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@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -371,8 +437,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -383,7 +449,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false - name: Create gh-aw temp directory @@ -392,23 +458,21 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" env: GH_TOKEN: ${{ github.token }} + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request || github.event.issue.pull_request + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -420,14 +484,14 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 env: GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} @@ -435,16 +499,11 @@ jobs: script: | const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: activation - path: /tmp/gh-aw - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -456,15 +515,15 @@ 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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_aebcb23c2a00f40b_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":1024,"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_aebcb23c2a00f40b_EOF + 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 Tools env: GH_AW_TOOLS_META_JSON: | @@ -560,7 +619,6 @@ jobs: "defaultMax": 1, "fields": { "branch": { - "required": true, "type": "string", "sanitize": true, "maxLength": 256 @@ -600,62 +658,24 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); await main(); - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - # Mask immediately to prevent timing vulnerabilities - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${API_KEY}" - - PORT=3001 - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" - - name: Start MCP Gateway id: start-mcp-gateway env: + GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }} GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + 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 }} 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 }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" - + # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" - export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_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}" @@ -664,29 +684,24 @@ jobs: mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" export DEBUG="*" - + export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - case "${DOCKER_HOST:-}" in - unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; - /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; - * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; - esac - DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.22' - - mkdir -p /home/runner/.copilot + 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.1' + + 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_8710e02016d3ca0b_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_89bf9ba8e0ab7f74_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.1.0", + "container": "ghcr.io/github/github-mcp-server:v1.5.0", "env": { - "GITHUB_HOST": "\${GITHUB_SERVER_URL}", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_HOST": "${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "context,repos" }, @@ -698,16 +713,35 @@ jobs: } }, "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + "type": "stdio", + "container": "ghcr.io/github/gh-aw-node", + "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], + "args": ["-w", "\${GITHUB_WORKSPACE}"], + "entrypoint": "sh", + "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"], + "env": { + "DEBUG": "*", + "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", + "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", + "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", + "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", + "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", + "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", + "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}", + "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", + "GITHUB_SHA": "\${GITHUB_SHA}", + "GITHUB_TOKEN": "\${GITHUB_TOKEN}", + "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", + "RUNNER_TEMP": "\${RUNNER_TEMP}" }, "guard-policies": { "write-sink": { "accept": [ "*" - ] + ], + "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} } } } @@ -719,7 +753,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_8710e02016d3ca0b_EOF + GH_AW_MCP_CONFIG_89bf9ba8e0ab7f74_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -748,41 +782,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["*.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","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + GH_AW_CHROOT_BINARIES_SOURCE_PATH="${RUNNER_TEMP}/gh-aw" GH_AW_CHROOT_IDENTITY_HOME="${RUNNER_TEMP}/gh-aw/home" node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs" fi GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --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 + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + 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_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 60 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -797,7 +841,8 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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: Detect agent errors if: always() id: detect-agent-errors @@ -805,17 +850,10 @@ jobs: run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Copy Copilot session state files to logs if: always() continue-on-error: true @@ -839,8 +877,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -860,7 +897,7 @@ 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,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.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" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: @@ -874,6 +911,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + 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'); @@ -895,16 +933,7 @@ jobs: continue-on-error: true env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_firewall_logs.sh" --rootless - name: Parse token usage for step summary if: always() continue-on-error: true @@ -965,17 +994,19 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_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') runs-on: ubuntu-slim permissions: contents: write - discussions: write issues: write pull-requests: write concurrency: group: "gh-aw-conclusion-java-adapt-handwritten-code-to-accept-upgrade-changes" cancel-in-progress: false queue: max + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -984,7 +1015,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -993,8 +1024,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1010,6 +1041,98 @@ jobs: 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 + id: download-safe-outputs-manifest + if: always() + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: safe-outputs-items + 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 + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: usage + path: | + /tmp/gh-aw/usage/aw_info.json + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.json + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/github_rate_limits.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + /tmp/gh-aw/usage/activity/summary.json + if-no-files-found: ignore + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache-conclusion + if: always() + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-javaadapthandwrittencodetoacceptupgradechanges-${{ github.run_id }} + restore-keys: agentic-workflow-usage-javaadapthandwrittencodetoacceptupgradechanges- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Write daily AIC usage cache entry + id: write-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + github-token: ${{ github.token }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context); + const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + await main(); + - name: Save daily AIC usage cache + id: save-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-javaadapthandwrittencodetoacceptupgradechanges-${{ github.run_id }} + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Upload daily AIC usage cache artifact + id: upload-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: aic-usage-cache + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + if-no-files-found: ignore + retention-days: 7 - name: Process no-op messages id: noop uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1021,6 +1144,10 @@ jobs: 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: "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 }} + GH_AW_WORKFLOW_ID: "java-adapt-handwritten-code-to-accept-upgrade-changes" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1088,25 +1215,32 @@ jobs: GH_AW_WORKFLOW_ID: "java-adapt-handwritten-code-to-accept-upgrade-changes" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" GH_AW_ENGINE_ID: "copilot" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} 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 }} GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "60" - GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1119,19 +1253,22 @@ jobs: needs: - activation - agent - if: > - always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + if: always() && needs.agent.result != 'skipped' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1140,8 +1277,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1159,7 +1296,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1168,7 +1305,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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 - name: Check if detection needed id: detection_guard if: always() @@ -1187,12 +1324,13 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.copilot/mcp-config.json" rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + 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." @@ -1230,11 +1368,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1244,39 +1382,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + _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:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + 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_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1290,7 +1440,21 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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() + 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'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1340,18 +1504,22 @@ jobs: runs-on: ubuntu-slim permissions: contents: write - discussions: write issues: write pull-requests: write - timeout-minutes: 15 + timeout-minutes: 45 env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/java-adapt-handwritten-code-to-accept-upgrade-changes" GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.55" + GH_AW_ENGINE_VERSION: "1.0.70" + 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" 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" @@ -1369,7 +1537,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1378,8 +1546,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1401,50 +1569,23 @@ jobs: with: name: agent path: /tmp/gh-aw/ - - name: Extract base branch from agent output - id: extract-base-branch - if: steps.download-agent-output.outcome == 'success' - 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/extract_base_branch_from_agent_output.cjs'); - await main(); - - name: Checkout repository (trusted default branch for comment events) - if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event.repository.default_branch }} - token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - persist-credentials: false - fetch-depth: 1 - name: Checkout repository - if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: - ref: ${{ steps.extract-base-branch.outputs.base-branch || github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }} + persist-credentials: true token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - persist-credentials: false - fetch-depth: 1 - name: Configure Git credentials if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash - run: | + run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. GH_HOST="${GITHUB_SERVER_URL#https://}" @@ -1456,10 +1597,10 @@ 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,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.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" 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\":1024,\"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\",\"CLAUDE.md\",\"GEMINI.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 }} @@ -1477,4 +1618,3 @@ jobs: /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.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 e3661607f6..b1623ff78a 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 @@ -20,6 +20,7 @@ permissions: contents: read actions: read + copilot-requests: write timeout-minutes: 60 network: @@ -34,7 +35,7 @@ tools: safe-outputs: push-to-pull-request-branch: target: "*" - labels: [dependencies, sdk/java] + required-labels: [dependencies, sdk/java] add-comment: target: "*" max: 10 @@ -155,4 +156,4 @@ git push origin "${{ inputs.branch }}" Then add a comment to PR #${{ inputs.pr_number }} summarizing what was fixed. -If after 3 full fix-compile-test cycles the build still fails, add a comment to the PR describing the remaining failures and stop. +If after 3 full fix-compile-test cycles the build still fails, add a comment to the PR describing the remaining failures and stop. \ No newline at end of file diff --git a/.github/workflows/java-codegen-fix.lock.yml b/.github/workflows/java-codegen-fix.lock.yml index 79d9505ec3..11e1b91842 100644 --- a/.github/workflows/java-codegen-fix.lock.yml +++ b/.github/workflows/java-codegen-fix.lock.yml @@ -1,20 +1,21 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"af13eefc935af6393de806a9a4307707d3b51a8c0d96992a84383cd9be790d52","body_hash":"fe41f8fe1c12cb585cfaefdd755f58d2a84410d9d819cd706a3192ce052e2545","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} -# 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/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b0390c9ab9beb0d7e106314299e89e486269ab7f64d8489d5021132d79aa6b9b","body_hash":"fe41f8fe1c12cb585cfaefdd755f58d2a84410d9d819cd706a3192ce052e2545","compiler_version":"v0.82.10","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}} +# 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":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"05205436a78512d71a2d842e46586ed05f4fa058","version":"v0.82.10"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"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.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} +# This file was automatically generated by gh-aw (v0.82.10). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ # | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ +# | | | | (_| | __/ | | | |_| | (__ # \_| |_/\__, |\___|_| |_|\__|_|\___| # __/ | -# _ _ |___/ +# _ _ |___/ # | | | | / _| | # | | | | ___ _ __ _ __| |_| | _____ ____ # | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -33,21 +34,24 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - 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/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# - github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 -# - ghcr.io/github/gh-aw-mcpg:v0.3.22 -# - ghcr.io/github/github-mcp-server:v1.1.0 -# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 +# - ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 +# - ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 +# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b +# - ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 name: "Java Codegen Agentic Fix" on: @@ -84,13 +88,19 @@ jobs: permissions: actions: read contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: comment_id: "" comment_repo: "" + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + 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 }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + oauth_token_check_failed: ${{ steps.check-oauth-tokens.outputs.oauth_token_check_failed == 'true' }} 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 }} @@ -98,15 +108,16 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -114,16 +125,16 @@ jobs: 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.55" - GH_AW_INFO_AGENT_VERSION: "1.0.55" - GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AGENT_VERSION: "1.0.70" + GH_AW_INFO_CLI_VERSION: "v0.82.10" 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.25.58" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -134,13 +145,59 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-javacodegenfix-${{ github.run_id }} + restore-keys: agentic-workflow-usage-javacodegenfix- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Restore daily AIC usage cache (artifact fallback) + id: restore-daily-aic-cache-fallback + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }} + GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }} + 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/restore_aic_usage_cache_fallback.cjs'); + await main(); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "Java Codegen Agentic Fix" + GH_AW_WORKFLOW_ID: "java-codegen-fix" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_HAS_SLASH_COMMAND: "false" + GH_AW_HAS_LABEL_COMMAND: "false" + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + 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_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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false sparse-checkout: | @@ -149,7 +206,6 @@ jobs: .antigravity .claude .codex - .crush .gemini .opencode .pi @@ -157,8 +213,8 @@ jobs: fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" - name: Check workflow lock file @@ -176,13 +232,16 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.77.5" + GH_AW_COMPILED_VERSION: "v0.82.10" 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/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 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -202,23 +261,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_1b89baae00b47687_EOF' + cat << 'GH_AW_PROMPT_7834a0b5f08e9149_EOF' - GH_AW_PROMPT_1b89baae00b47687_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_1b89baae00b47687_EOF' + cat << 'GH_AW_PROMPT_7834a0b5f08e9149_EOF' Tools: add_comment(max:5), push_to_pull_request_branch, missing_tool, missing_data, noop - GH_AW_PROMPT_1b89baae00b47687_EOF + GH_AW_PROMPT_7834a0b5f08e9149_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_1b89baae00b47687_EOF' + cat << 'GH_AW_PROMPT_7834a0b5f08e9149_EOF' - GH_AW_PROMPT_1b89baae00b47687_EOF + GH_AW_PROMPT_7834a0b5f08e9149_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_1b89baae00b47687_EOF' + cat << 'GH_AW_PROMPT_7834a0b5f08e9149_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -246,13 +305,13 @@ jobs: - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - - GH_AW_PROMPT_1b89baae00b47687_EOF + + GH_AW_PROMPT_7834a0b5f08e9149_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_1b89baae00b47687_EOF' + cat << 'GH_AW_PROMPT_7834a0b5f08e9149_EOF' {{#runtime-import .github/workflows/java-codegen-fix.md}} - GH_AW_PROMPT_1b89baae00b47687_EOF + GH_AW_PROMPT_7834a0b5f08e9149_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -288,9 +347,9 @@ jobs: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io, getOctokit); - + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); - + // Call the substitution function return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, @@ -327,7 +386,7 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json @@ -340,23 +399,29 @@ jobs: agent: needs: activation + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest permissions: actions: read contents: read + copilot-requests: write 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_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: javacodegenfix outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} + 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' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} @@ -366,10 +431,11 @@ 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 }} + 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@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -378,8 +444,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -390,7 +456,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false - name: Create gh-aw temp directory @@ -399,23 +465,21 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" env: GH_TOKEN: ${{ github.token }} + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request || github.event.issue.pull_request + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -427,14 +491,14 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 env: GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} @@ -442,16 +506,11 @@ jobs: script: | const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: activation - path: /tmp/gh-aw - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -463,15 +522,15 @@ 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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_9547b36a6c2ad8b6_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":1024,"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_9547b36a6c2ad8b6_EOF + 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 Tools env: GH_AW_TOOLS_META_JSON: | @@ -567,7 +626,6 @@ jobs: "defaultMax": 1, "fields": { "branch": { - "required": true, "type": "string", "sanitize": true, "maxLength": 256 @@ -607,62 +665,24 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); await main(); - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - # Mask immediately to prevent timing vulnerabilities - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${API_KEY}" - - PORT=3001 - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" - - name: Start MCP Gateway id: start-mcp-gateway env: + GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }} GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + 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 }} 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 }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" - + # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" - export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_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}" @@ -671,29 +691,24 @@ jobs: mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" export DEBUG="*" - + export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - case "${DOCKER_HOST:-}" in - unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; - /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; - * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; - esac - DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.22' - - mkdir -p /home/runner/.copilot + 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.1' + + 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_209c8aba9155ceb2_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_89bf9ba8e0ab7f74_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.1.0", + "container": "ghcr.io/github/github-mcp-server:v1.5.0", "env": { - "GITHUB_HOST": "\${GITHUB_SERVER_URL}", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_HOST": "${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "context,repos" }, @@ -705,16 +720,35 @@ jobs: } }, "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + "type": "stdio", + "container": "ghcr.io/github/gh-aw-node", + "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], + "args": ["-w", "\${GITHUB_WORKSPACE}"], + "entrypoint": "sh", + "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"], + "env": { + "DEBUG": "*", + "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", + "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", + "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", + "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", + "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", + "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", + "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}", + "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", + "GITHUB_SHA": "\${GITHUB_SHA}", + "GITHUB_TOKEN": "\${GITHUB_TOKEN}", + "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", + "RUNNER_TEMP": "\${RUNNER_TEMP}" }, "guard-policies": { "write-sink": { "accept": [ "*" - ] + ], + "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} } } } @@ -726,7 +760,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_209c8aba9155ceb2_EOF + GH_AW_MCP_CONFIG_89bf9ba8e0ab7f74_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -755,41 +789,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["*.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","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + GH_AW_CHROOT_BINARIES_SOURCE_PATH="${RUNNER_TEMP}/gh-aw" GH_AW_CHROOT_IDENTITY_HOME="${RUNNER_TEMP}/gh-aw/home" node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs" fi GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --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 + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + 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_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 60 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -804,7 +848,8 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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: Detect agent errors if: always() id: detect-agent-errors @@ -812,17 +857,10 @@ jobs: run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Copy Copilot session state files to logs if: always() continue-on-error: true @@ -846,8 +884,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -867,7 +904,7 @@ 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,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.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" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: @@ -881,6 +918,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + 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'); @@ -902,16 +940,7 @@ jobs: continue-on-error: true env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_firewall_logs.sh" --rootless - name: Parse token usage for step summary if: always() continue-on-error: true @@ -972,17 +1001,19 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_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') runs-on: ubuntu-slim permissions: contents: write - discussions: write issues: write pull-requests: write concurrency: group: "gh-aw-conclusion-java-codegen-fix" cancel-in-progress: false queue: max + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -991,7 +1022,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1000,8 +1031,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1017,6 +1048,98 @@ jobs: 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 + id: download-safe-outputs-manifest + if: always() + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: safe-outputs-items + 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 + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: usage + path: | + /tmp/gh-aw/usage/aw_info.json + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.json + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/github_rate_limits.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + /tmp/gh-aw/usage/activity/summary.json + if-no-files-found: ignore + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache-conclusion + if: always() + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-javacodegenfix-${{ github.run_id }} + restore-keys: agentic-workflow-usage-javacodegenfix- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Write daily AIC usage cache entry + id: write-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + github-token: ${{ github.token }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context); + const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + await main(); + - name: Save daily AIC usage cache + id: save-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-javacodegenfix-${{ github.run_id }} + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Upload daily AIC usage cache artifact + id: upload-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: aic-usage-cache + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + if-no-files-found: ignore + retention-days: 7 - name: Process no-op messages id: noop uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1028,6 +1151,10 @@ jobs: 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: "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 }} + GH_AW_WORKFLOW_ID: "java-codegen-fix" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1095,25 +1222,32 @@ jobs: GH_AW_WORKFLOW_ID: "java-codegen-fix" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" GH_AW_ENGINE_ID: "copilot" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} 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 }} GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "60" - GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1126,19 +1260,22 @@ jobs: needs: - activation - agent - if: > - always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + if: always() && needs.agent.result != 'skipped' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1147,8 +1284,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1166,7 +1303,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1175,7 +1312,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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 - name: Check if detection needed id: detection_guard if: always() @@ -1194,12 +1331,13 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.copilot/mcp-config.json" rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + 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." @@ -1237,11 +1375,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1251,39 +1389,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + _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:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + 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_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1297,7 +1447,21 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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() + 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'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1347,18 +1511,22 @@ jobs: runs-on: ubuntu-slim permissions: contents: write - discussions: write issues: write pull-requests: write - timeout-minutes: 15 + timeout-minutes: 45 env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/java-codegen-fix" GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.55" + GH_AW_ENGINE_VERSION: "1.0.70" + 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" 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" @@ -1376,7 +1544,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1385,8 +1553,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1408,50 +1576,23 @@ jobs: with: name: agent path: /tmp/gh-aw/ - - name: Extract base branch from agent output - id: extract-base-branch - if: steps.download-agent-output.outcome == 'success' - 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/extract_base_branch_from_agent_output.cjs'); - await main(); - - name: Checkout repository (trusted default branch for comment events) - if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event.repository.default_branch }} - token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - persist-credentials: false - fetch-depth: 1 - name: Checkout repository - if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: - ref: ${{ steps.extract-base-branch.outputs.base-branch || github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }} + persist-credentials: true token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - persist-credentials: false - fetch-depth: 1 - name: Configure Git credentials if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash - run: | + run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. GH_HOST="${GITHUB_SERVER_URL#https://}" @@ -1463,10 +1604,10 @@ 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,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.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" 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\":1024,\"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\",\"CLAUDE.md\",\"GEMINI.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 }} @@ -1484,4 +1625,3 @@ jobs: /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - diff --git a/.github/workflows/java-codegen-fix.md b/.github/workflows/java-codegen-fix.md index 882df1d4a7..1fe465f4c5 100644 --- a/.github/workflows/java-codegen-fix.md +++ b/.github/workflows/java-codegen-fix.md @@ -23,6 +23,7 @@ permissions: contents: read actions: read + copilot-requests: write timeout-minutes: 60 network: @@ -37,13 +38,14 @@ tools: safe-outputs: push-to-pull-request-branch: target: "*" - labels: [dependencies] + required-labels: [dependencies] add-comment: target: "*" max: 5 noop: report-as-issue: false --- + # Java Codegen Agentic Fix You are an automation agent that fixes Java compilation and test failures caused by code generation changes in the `copilot-sdk` monorepo. @@ -242,4 +244,4 @@ Do **NOT** push broken code. - You **MAY** modify files under `java/src/main/java/` and `java/src/test/java/` to fix handwritten code - Always run `cd java && mvn spotless:apply` before committing to ensure code formatting - Maximum 3 fix attempts before reporting failure via `noop` -- Only push if `mvn verify` passes +- Only push if `mvn verify` passes \ No newline at end of file diff --git a/.github/workflows/java-publish-maven.yml b/.github/workflows/java-publish-maven.yml index e22cd05a5b..944a0ee38e 100644 --- a/.github/workflows/java-publish-maven.yml +++ b/.github/workflows/java-publish-maven.yml @@ -22,6 +22,34 @@ on: type: boolean required: false default: false + workflow_call: + inputs: + releaseVersion: + description: "Release version (e.g., 1.0.0). If empty, derives from pom.xml by removing -SNAPSHOT" + required: false + type: string + developmentVersion: + description: "Next development version (e.g., 1.0.1-SNAPSHOT). If empty, increments patch version" + required: false + type: string + prerelease: + description: "Is this a prerelease?" + type: boolean + required: false + default: false + secrets: + JAVA_RELEASE_TOKEN: + required: true + JAVA_RELEASE_GITHUB_TOKEN: + required: true + JAVA_MAVEN_CENTRAL_USERNAME: + required: true + JAVA_MAVEN_CENTRAL_PASSWORD: + required: true + JAVA_GPG_SECRET_KEY: + required: true + JAVA_GPG_PASSPHRASE: + required: true permissions: contents: write @@ -144,10 +172,10 @@ jobs: exit 1 fi else - # Split version: supports "0.1.32", "0.1.32-java.0", and "0.1.32-java-preview.0" formats + # Split version: supports "0.1.32", "0.1.32-preview.0", "0.1.32-java.0", and "0.1.32-java-preview.0" formats # Validate RELEASE_VERSION format explicitly to provide clear errors - if ! echo "$RELEASE_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-(beta-)?java(-preview)?\.[0-9]+)?$'; then - echo "Error: RELEASE_VERSION '$RELEASE_VERSION' is invalid. Expected format: M.M.P, M.M.P-java.N, M.M.P-java-preview.N, M.M.P-beta-java.N, or M.M.P-beta-java-preview.N (e.g., 1.2.3, 1.2.3-java.0, 1.2.3-java-preview.0, 1.2.3-beta-java.0, or 1.2.3-beta-java-preview.0)." >&2 + if ! echo "$RELEASE_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-(preview|(beta-)?java(-preview)?)\.[0-9]+)?$'; then + echo "Error: RELEASE_VERSION '$RELEASE_VERSION' is invalid. Expected format: M.M.P, M.M.P-preview.N, M.M.P-java.N, M.M.P-java-preview.N, M.M.P-beta-java.N, or M.M.P-beta-java-preview.N (e.g., 1.2.3, 1.2.3-preview.0, 1.2.3-java.0, 1.2.3-java-preview.0, 1.2.3-beta-java.0, or 1.2.3-beta-java-preview.0)." >&2 exit 1 fi # Extract the base M.M.P portion (before any qualifier) diff --git a/.github/workflows/nodejs-sdk-tests.yml b/.github/workflows/nodejs-sdk-tests.yml index 8880cadfa3..647345e0ea 100644 --- a/.github/workflows/nodejs-sdk-tests.yml +++ b/.github/workflows/nodejs-sdk-tests.yml @@ -31,7 +31,7 @@ permissions: jobs: test: - name: "Node.js SDK Tests" + name: "Node.js SDK Tests (${{ matrix.os }}, ${{ matrix.transport }})" if: github.event.repository.fork == false env: POWERSHELL_UPDATECHECK: Off @@ -39,6 +39,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] + transport: ["default", "inprocess"] runs-on: ${{ matrix.os }} defaults: run: @@ -75,6 +76,11 @@ jobs: if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" + - name: Select inprocess transport + if: matrix.transport == 'inprocess' + run: | + echo "COPILOT_SDK_DEFAULT_CONNECTION=inprocess" >> "$GITHUB_ENV" + - name: Run Node.js SDK tests env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e5b23a5816..cd96dd0fa9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -171,13 +171,17 @@ jobs: - name: Set version run: sed -i -E 's/^version = ".*"$/version = "${{ needs.version.outputs.version }}"/' Cargo.toml - name: Snapshot CLI version + hashes for build.rs - run: bash scripts/snapshot-bundled-cli-version.sh - - name: Verify cli-version.txt exists run: | - if [[ ! -f cli-version.txt ]]; then - echo "::error::cli-version.txt was not generated. The Snapshot step must run before packaging." - exit 1 - fi + bash scripts/snapshot-bundled-cli-version.sh + bash scripts/snapshot-bundled-in-process-version.sh + - name: Verify CLI version snapshots exist + run: | + for snapshot in cli-version.txt cli-version-in-process.txt; do + if [[ ! -f "${snapshot}" ]]; then + echo "::error::${snapshot} was not generated. The Snapshot step must run before packaging." + exit 1 + fi + done - name: Package (dry run) run: cargo publish --dry-run --allow-dirty - name: Upload artifact @@ -229,10 +233,28 @@ jobs: with: packages-dir: python/dist/ + publish-java: + name: Publish Java SDK + if: github.event.inputs.dist-tag != 'unstable' && github.ref == 'refs/heads/main' + needs: version + uses: ./.github/workflows/java-publish-maven.yml + with: + releaseVersion: ${{ needs.version.outputs.version }} + prerelease: ${{ github.event.inputs.dist-tag == 'prerelease' }} + secrets: inherit + github-release: name: Create GitHub Release - needs: [version, publish-nodejs, publish-dotnet, publish-python, publish-rust] - if: github.ref == 'refs/heads/main' && github.event.inputs.dist-tag != 'unstable' + needs: [version, publish-nodejs, publish-dotnet, publish-python, publish-rust, publish-java] + if: | + always() && + github.ref == 'refs/heads/main' && + github.event.inputs.dist-tag != 'unstable' && + needs.version.result == 'success' && + needs.publish-nodejs.result == 'success' && + needs.publish-dotnet.result == 'success' && + needs.publish-python.result == 'success' && + needs.publish-rust.result == 'success' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6.0.2 diff --git a/.github/workflows/python-sdk-tests.yml b/.github/workflows/python-sdk-tests.yml index e6260dd0b2..8d3ea07154 100644 --- a/.github/workflows/python-sdk-tests.yml +++ b/.github/workflows/python-sdk-tests.yml @@ -31,7 +31,7 @@ permissions: jobs: test: - name: "Python SDK Tests" + name: "Python SDK Tests (${{ matrix.os }}, ${{ matrix.transport }})" if: github.event.repository.fork == false env: POWERSHELL_UPDATECHECK: Off @@ -41,6 +41,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] # Test the oldest supported Python version to make sure compatibility is maintained. python-version: ["3.11"] + transport: ["default", "inprocess"] runs-on: ${{ matrix.os }} defaults: run: @@ -86,6 +87,11 @@ jobs: if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" + - name: Select inprocess transport + if: matrix.transport == 'inprocess' + run: | + echo "COPILOT_SDK_DEFAULT_CONNECTION=inprocess" >> "$GITHUB_ENV" + - name: Run Python SDK tests env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} diff --git a/.github/workflows/release-changelog.lock.yml b/.github/workflows/release-changelog.lock.yml index 4e8adbc0c2..f6ea436049 100644 --- a/.github/workflows/release-changelog.lock.yml +++ b/.github/workflows/release-changelog.lock.yml @@ -1,20 +1,21 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"f56148e477b1349cf894dd5ee148dae8af3a90ab64cf708a41697d2c13b2da4b","body_hash":"89e26ed929f440bd6af57d1da92b06dbf1739b4a1d34b9923286919d00f272d1","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} -# 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/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"9342b428009e6a3b47258c08b78735a89fc72714b73a44b72c4714e310d60006","body_hash":"89e26ed929f440bd6af57d1da92b06dbf1739b4a1d34b9923286919d00f272d1","compiler_version":"v0.82.10","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}} +# 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":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"05205436a78512d71a2d842e46586ed05f4fa058","version":"v0.82.10"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"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.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} +# This file was automatically generated by gh-aw (v0.82.10). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ # | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ +# | | | | (_| | __/ | | | |_| | (__ # \_| |_/\__, |\___|_| |_|\__|_|\___| # __/ | -# _ _ |___/ +# _ _ |___/ # | | | | / _| | # | | | | ___ _ __ _ __| |_| | _____ ____ # | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -32,21 +33,24 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - 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/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# - github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 -# - ghcr.io/github/gh-aw-mcpg:v0.3.22 -# - ghcr.io/github/github-mcp-server:v1.1.0 -# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 +# - ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 +# - ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 +# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b +# - ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 name: "Release Changelog Generator" on: @@ -75,13 +79,19 @@ jobs: permissions: actions: read contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: comment_id: "" comment_repo: "" + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + 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 }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + oauth_token_check_failed: ${{ steps.check-oauth-tokens.outputs.oauth_token_check_failed == 'true' }} 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 }} @@ -89,15 +99,16 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -105,16 +116,16 @@ jobs: 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.55" - GH_AW_INFO_AGENT_VERSION: "1.0.55" - GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AGENT_VERSION: "1.0.70" + GH_AW_INFO_CLI_VERSION: "v0.82.10" 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.25.58" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -125,13 +136,59 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-releasechangelog-${{ github.run_id }} + restore-keys: agentic-workflow-usage-releasechangelog- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Restore daily AIC usage cache (artifact fallback) + id: restore-daily-aic-cache-fallback + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }} + GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }} + 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/restore_aic_usage_cache_fallback.cjs'); + await main(); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "Release Changelog Generator" + GH_AW_WORKFLOW_ID: "release-changelog" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_HAS_SLASH_COMMAND: "false" + GH_AW_HAS_LABEL_COMMAND: "false" + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + 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_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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false sparse-checkout: | @@ -140,7 +197,6 @@ jobs: .antigravity .claude .codex - .crush .gemini .opencode .pi @@ -148,8 +204,8 @@ jobs: fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" - name: Check workflow lock file @@ -167,13 +223,16 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.77.5" + GH_AW_COMPILED_VERSION: "v0.82.10" 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/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 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -191,23 +250,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF' + cat << 'GH_AW_PROMPT_c642707f673b9ac4_EOF' - GH_AW_PROMPT_8ca4e2fb6c3e0923_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_8ca4e2fb6c3e0923_EOF' + cat << 'GH_AW_PROMPT_c642707f673b9ac4_EOF' Tools: create_pull_request, update_release, missing_tool, missing_data, noop - GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF + GH_AW_PROMPT_c642707f673b9ac4_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF' + cat << 'GH_AW_PROMPT_c642707f673b9ac4_EOF' - GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF + GH_AW_PROMPT_c642707f673b9ac4_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF' + cat << 'GH_AW_PROMPT_c642707f673b9ac4_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -235,13 +294,13 @@ jobs: - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - - GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF + + GH_AW_PROMPT_c642707f673b9ac4_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF' + cat << 'GH_AW_PROMPT_c642707f673b9ac4_EOF' {{#runtime-import .github/workflows/release-changelog.md}} - GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF + GH_AW_PROMPT_c642707f673b9ac4_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -274,9 +333,9 @@ jobs: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io, getOctokit); - + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); - + // Call the substitution function return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, @@ -311,7 +370,7 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json @@ -324,10 +383,12 @@ jobs: agent: needs: activation + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest permissions: actions: read contents: read + copilot-requests: write issues: read pull-requests: read env: @@ -336,13 +397,17 @@ jobs: GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: releasechangelog outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} + 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' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} @@ -352,10 +417,11 @@ 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 }} + 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@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -364,8 +430,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -376,7 +442,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false - name: Create gh-aw temp directory @@ -385,23 +451,21 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" env: GH_TOKEN: ${{ github.token }} + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request || github.event.issue.pull_request + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -413,14 +477,14 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 env: GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} @@ -428,16 +492,11 @@ jobs: script: | const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: activation - path: /tmp/gh-aw - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -449,15 +508,15 @@ 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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_6e92a7a47fdc567f_EOF' - {"create_pull_request":{"draft":false,"labels":["automation","changelog"],"max":1,"max_patch_files":100,"max_patch_size":1024,"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_6e92a7a47fdc567f_EOF + 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 Tools env: GH_AW_TOOLS_META_JSON: | @@ -592,7 +651,8 @@ jobs: "required": true, "type": "string", "sanitize": true, - "maxLength": 65000 + "maxLength": 65000, + "minLength": 20 }, "operation": { "required": true, @@ -618,62 +678,24 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); await main(); - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - # Mask immediately to prevent timing vulnerabilities - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${API_KEY}" - - PORT=3001 - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" - - name: Start MCP Gateway id: start-mcp-gateway env: + GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }} GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + 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 }} 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 }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" - + # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" - export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_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}" @@ -682,29 +704,24 @@ jobs: mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" export DEBUG="*" - + export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - case "${DOCKER_HOST:-}" in - unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; - /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; - * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; - esac - DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.22' - - mkdir -p /home/runner/.copilot + 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.1' + + 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_432de5cac6e63f96_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_d97c92af15acf38e_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.1.0", + "container": "ghcr.io/github/github-mcp-server:v1.5.0", "env": { - "GITHUB_HOST": "\${GITHUB_SERVER_URL}", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_HOST": "${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" }, @@ -716,16 +733,35 @@ jobs: } }, "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + "type": "stdio", + "container": "ghcr.io/github/gh-aw-node", + "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], + "args": ["-w", "\${GITHUB_WORKSPACE}"], + "entrypoint": "sh", + "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"], + "env": { + "DEBUG": "*", + "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", + "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", + "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", + "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", + "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", + "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", + "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}", + "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", + "GITHUB_SHA": "\${GITHUB_SHA}", + "GITHUB_TOKEN": "\${GITHUB_TOKEN}", + "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", + "RUNNER_TEMP": "\${RUNNER_TEMP}" }, "guard-policies": { "write-sink": { "accept": [ "*" - ] + ], + "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} } } } @@ -737,7 +773,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_432de5cac6e63f96_EOF + GH_AW_MCP_CONFIG_d97c92af15acf38e_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -766,41 +802,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + GH_AW_CHROOT_BINARIES_SOURCE_PATH="${RUNNER_TEMP}/gh-aw" GH_AW_CHROOT_IDENTITY_HOME="${RUNNER_TEMP}/gh-aw/home" node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs" fi GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --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 + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + 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_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 15 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -815,7 +861,8 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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: Detect agent errors if: always() id: detect-agent-errors @@ -823,17 +870,10 @@ jobs: run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Copy Copilot session state files to logs if: always() continue-on-error: true @@ -857,8 +897,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -892,6 +931,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + 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'); @@ -913,16 +953,7 @@ jobs: continue-on-error: true env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_firewall_logs.sh" --rootless - name: Parse token usage for step summary if: always() continue-on-error: true @@ -983,7 +1014,8 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_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') runs-on: ubuntu-slim permissions: contents: write @@ -993,6 +1025,8 @@ jobs: group: "gh-aw-conclusion-release-changelog" cancel-in-progress: false queue: max + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -1001,7 +1035,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1010,8 +1044,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1027,6 +1061,98 @@ jobs: 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 + id: download-safe-outputs-manifest + if: always() + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: safe-outputs-items + 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 + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: usage + path: | + /tmp/gh-aw/usage/aw_info.json + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.json + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/github_rate_limits.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + /tmp/gh-aw/usage/activity/summary.json + if-no-files-found: ignore + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache-conclusion + if: always() + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-releasechangelog-${{ github.run_id }} + restore-keys: agentic-workflow-usage-releasechangelog- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Write daily AIC usage cache entry + id: write-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + github-token: ${{ github.token }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context); + const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + await main(); + - name: Save daily AIC usage cache + id: save-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-releasechangelog-${{ github.run_id }} + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Upload daily AIC usage cache artifact + id: upload-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: aic-usage-cache + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + if-no-files-found: ignore + retention-days: 7 - name: Process no-op messages id: noop uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1038,6 +1164,10 @@ jobs: 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_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_WORKFLOW_ID: "release-changelog" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1105,25 +1235,32 @@ jobs: GH_AW_WORKFLOW_ID: "release-changelog" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" GH_AW_ENGINE_ID: "copilot" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} 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 }} GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "15" - GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1136,19 +1273,22 @@ jobs: needs: - activation - agent - if: > - always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + if: always() && needs.agent.result != 'skipped' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1157,8 +1297,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1176,7 +1316,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1185,7 +1325,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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 - name: Check if detection needed id: detection_guard if: always() @@ -1204,12 +1344,13 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.copilot/mcp-config.json" rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + 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." @@ -1247,11 +1388,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1261,39 +1402,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + _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:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + 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_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1307,7 +1460,21 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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() + 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'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1359,15 +1526,20 @@ jobs: contents: write issues: write pull-requests: write - timeout-minutes: 15 + timeout-minutes: 45 env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/release-changelog" GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.55" + GH_AW_ENGINE_VERSION: "1.0.70" + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "release-changelog" 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" @@ -1383,7 +1555,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1392,8 +1564,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1415,50 +1587,23 @@ jobs: with: name: agent path: /tmp/gh-aw/ - - name: Extract base branch from agent output - id: extract-base-branch - if: steps.download-agent-output.outcome == 'success' - 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/extract_base_branch_from_agent_output.cjs'); - await main(); - - name: Checkout repository (trusted default branch for comment events) - if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event.repository.default_branch }} - token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - persist-credentials: false - fetch-depth: 1 - name: Checkout repository - if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: - ref: ${{ steps.extract-base-branch.outputs.base-branch || github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }} + persist-credentials: true token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - persist-credentials: false - fetch-depth: 1 - name: Configure Git credentials if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash - run: | + run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. GH_HOST="${GITHUB_SERVER_URL#https://}" @@ -1473,7 +1618,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"draft\":false,\"labels\":[\"automation\",\"changelog\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"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\",\"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_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1491,4 +1636,3 @@ jobs: /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - diff --git a/.github/workflows/release-changelog.md b/.github/workflows/release-changelog.md index 52af777cd8..7a682c56dd 100644 --- a/.github/workflows/release-changelog.md +++ b/.github/workflows/release-changelog.md @@ -12,6 +12,7 @@ permissions: actions: read issues: read pull-requests: read + copilot-requests: write tools: github: toolsets: [default] diff --git a/.github/workflows/rust-sdk-tests.yml b/.github/workflows/rust-sdk-tests.yml index f75a5d6a29..8e13e16b23 100644 --- a/.github/workflows/rust-sdk-tests.yml +++ b/.github/workflows/rust-sdk-tests.yml @@ -29,7 +29,7 @@ permissions: jobs: test: - name: "Rust SDK Tests" + name: "Rust SDK Tests (${{ matrix.os }}, default)" if: github.event.repository.fork == false env: POWERSHELL_UPDATECHECK: Off @@ -84,7 +84,7 @@ jobs: # Share the bundled-CLI archive cache with the `bundle` job: build.rs # now downloads in both modes (embed for `bundle`, extract-to-cache # for this `test` job's `--no-default-features` build). - - name: Cache bundled CLI tarball + - name: Cache bundled CLI archives uses: actions/cache@v4 with: path: ./rust/.bundled-cli-cache @@ -98,7 +98,7 @@ jobs: if: runner.os == 'Linux' env: BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache - run: cargo clippy --all-targets --features test-support -- --no-deps -D warnings -D clippy::unwrap_used -D clippy::disallowed_macros -D clippy::await_holding_invalid_type + 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' @@ -129,6 +129,84 @@ jobs: # The dedicated `bundle` job below exercises the embed pipeline. run: cargo test --no-default-features --features test-support -- --test-threads=4 --nocapture + # 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 + # COPILOT_SDK_DEFAULT_CONNECTION=inprocess so the client hosts the runtime + # cdylib in-process instead of spawning a stdio child, then runs the whole + # E2E suite over the in-process transport. The suite runs serially in-process + # (the harness forces concurrency to 1) because it mirrors each test's + # environment onto the shared process environment the in-process worker inherits. + # Runs the whole E2E suite over the in-process transport on supported hosts. + test-inprocess: + name: "Rust SDK Tests (${{ matrix.os }}, inprocess)" + if: github.event.repository.fork == false + env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + strategy: + fail-fast: false + matrix: + # TODO: Re-enable Windows after fixing the napi-oop peer shutdown crash. + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash + working-directory: ./rust + steps: + - uses: actions/checkout@v6.0.2 + + - uses: ./.github/actions/setup-copilot + id: setup-copilot + + - 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 @github/copilot CLI version + id: cli-version + working-directory: ./nodejs + run: | + version=$(node -p "require('./package-lock.json').packages['node_modules/@github/copilot'].version") + 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-${{ matrix.os }}-${{ steps.cli-version.outputs.version }} + + - name: Install test harness dependencies + working-directory: ./test/harness + run: npm ci --ignore-scripts + + - name: Warm up PowerShell + if: runner.os == 'Windows' + run: pwsh.exe -Command "Write-Host 'PowerShell ready'" + + - name: Select in-process transport + run: echo "COPILOT_SDK_DEFAULT_CONNECTION=inprocess" >> "$GITHUB_ENV" + + - name: cargo test (in-process transport, full E2E suite) + timeout-minutes: 60 + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + COPILOT_CLI_PATH: ${{ steps.setup-copilot.outputs.cli-path }} + BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache + # The harness forces serial execution in-process (both the async semaphore and + # libtest via --test-threads=1) because it mirrors each test's environment onto + # the shared process environment, so RUST_E2E_CONCURRENCY is not set here. + run: cargo test --no-default-features --features test-support,bundled-in-process --test e2e -- --test-threads=1 --nocapture + # Validates the bundled-CLI build path on all three supported # platforms. While the regular `cargo test` job above also exercises # build.rs (bundling is on by default now), this matrix job is the @@ -136,7 +214,7 @@ jobs: # extract / embed pipeline. Catches regressions before they ship to # crates.io and before bundling consumers hit them downstream. bundle: - name: "Rust SDK Bundled CLI Build" + name: "Rust SDK Bundled CLI Build (${{ matrix.os }})" if: github.event.repository.fork == false env: CARGO_TERM_COLOR: always @@ -180,13 +258,15 @@ jobs: # ~130 MB on every CI invocation. Keyed by OS + CLI version so old # archives drop out when the pinned version bumps, keeping the # cache bounded. - - name: Cache bundled CLI tarball + - name: Cache bundled CLI archives uses: actions/cache@v4 with: path: ./rust/.bundled-cli-cache key: bundled-cli-${{ matrix.os }}-${{ steps.cli-version.outputs.version }} - - name: cargo build (bundled-cli is the default feature) + - name: Test bundled CLI build paths env: BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache - run: cargo build + run: | + cargo build + cargo test --features bundled-in-process --lib embedded_archive_contains_only_expected_files diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml new file mode 100644 index 0000000000..95f8b1c926 --- /dev/null +++ b/.github/workflows/sdk-canary.yml @@ -0,0 +1,391 @@ +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" diff --git a/.github/workflows/sdk-consistency-review.lock.yml b/.github/workflows/sdk-consistency-review.lock.yml index 3cffd9d387..f3dabb7ebb 100644 --- a/.github/workflows/sdk-consistency-review.lock.yml +++ b/.github/workflows/sdk-consistency-review.lock.yml @@ -1,20 +1,21 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"7705f1f36359046432427c7379dff5ce45f49f1d69e23433af41c1234042e51b","body_hash":"97333b6724b46fcc7c9d59c1eec7c424d3a2005fab0e52e2520c97a02021426b","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"fb73d13f101fc375308576a64180f63934cc9e8306cb6ef6303f1b9788d9df28","body_hash":"97333b6724b46fcc7c9d59c1eec7c424d3a2005fab0e52e2520c97a02021426b","compiler_version":"v0.82.10","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}} +# 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":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"05205436a78512d71a2d842e46586ed05f4fa058","version":"v0.82.10"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"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.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}],"has_pull_request":true} +# This file was automatically generated by gh-aw (v0.82.10). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ # | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ +# | | | | (_| | __/ | | | |_| | (__ # \_| |_/\__, |\___|_| |_|\__|_|\___| # __/ | -# _ _ |___/ +# _ _ |___/ # | | | | / _| | # | | | | ___ _ __ _ __| |_| | _____ ____ # | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -31,38 +32,41 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - 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/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# - github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 -# - ghcr.io/github/gh-aw-mcpg:v0.3.22 -# - ghcr.io/github/github-mcp-server:v1.1.0 -# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 +# - ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 +# - ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 +# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b +# - ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 name: "SDK Consistency Review Agent" on: pull_request: paths: - - nodejs/** - - python/** - - go/** - - dotnet/** - - java/** - - "!java/docs/**" - - "!java/*.txt" - - "!java/*.md" + - nodejs/** + - python/** + - go/** + - dotnet/** + - java/** + - "!java/docs/**" + - "!java/*.txt" + - "!java/*.md" types: - - opened - - synchronize - - reopened + - opened + - synchronize + - reopened # roles: all # Roles processed as role check in pre-activation job workflow_dispatch: inputs: @@ -91,14 +95,20 @@ jobs: permissions: actions: read contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: body: ${{ steps.sanitized.outputs.body }} comment_id: "" comment_repo: "" + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + 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 }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + oauth_token_check_failed: ${{ steps.check-oauth-tokens.outputs.oauth_token_check_failed == 'true' }} 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 }} @@ -108,15 +118,16 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -124,16 +135,16 @@ jobs: 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.55" - GH_AW_INFO_AGENT_VERSION: "1.0.55" - GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AGENT_VERSION: "1.0.70" + GH_AW_INFO_CLI_VERSION: "v0.82.10" 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.25.58" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -144,13 +155,59 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-sdkconsistencyreview-${{ github.run_id }} + restore-keys: agentic-workflow-usage-sdkconsistencyreview- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Restore daily AIC usage cache (artifact fallback) + id: restore-daily-aic-cache-fallback + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }} + GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }} + 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/restore_aic_usage_cache_fallback.cjs'); + await main(); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "SDK Consistency Review Agent" + GH_AW_WORKFLOW_ID: "sdk-consistency-review" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_HAS_SLASH_COMMAND: "false" + GH_AW_HAS_LABEL_COMMAND: "false" + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + 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_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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false sparse-checkout: | @@ -159,7 +216,6 @@ jobs: .antigravity .claude .codex - .crush .gemini .opencode .pi @@ -167,8 +223,8 @@ jobs: fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" - name: Check workflow lock file @@ -186,7 +242,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.77.5" + GH_AW_COMPILED_VERSION: "v0.82.10" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -204,6 +260,9 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/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 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -222,20 +281,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_cf79d4d226819b37_EOF' + cat << 'GH_AW_PROMPT_96d45caa4ffc7593_EOF' - GH_AW_PROMPT_cf79d4d226819b37_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_cf79d4d226819b37_EOF' + cat << 'GH_AW_PROMPT_96d45caa4ffc7593_EOF' Tools: add_comment, create_pull_request_review_comment(max:10), missing_tool, missing_data, noop - GH_AW_PROMPT_cf79d4d226819b37_EOF + GH_AW_PROMPT_96d45caa4ffc7593_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_cf79d4d226819b37_EOF' + cat << 'GH_AW_PROMPT_96d45caa4ffc7593_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -263,13 +322,13 @@ jobs: - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - - GH_AW_PROMPT_cf79d4d226819b37_EOF + + GH_AW_PROMPT_96d45caa4ffc7593_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_cf79d4d226819b37_EOF' + cat << 'GH_AW_PROMPT_96d45caa4ffc7593_EOF' {{#runtime-import .github/workflows/sdk-consistency-review.md}} - GH_AW_PROMPT_cf79d4d226819b37_EOF + GH_AW_PROMPT_96d45caa4ffc7593_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -304,9 +363,9 @@ jobs: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io, getOctokit); - + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); - + // Call the substitution function return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, @@ -342,7 +401,7 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json @@ -355,9 +414,11 @@ jobs: agent: needs: activation + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write issues: read pull-requests: read env: @@ -366,13 +427,17 @@ jobs: GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: sdkconsistencyreview outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} + 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' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} @@ -382,10 +447,11 @@ 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 }} + 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@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -394,8 +460,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -406,7 +472,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false - name: Create gh-aw temp directory @@ -415,23 +481,21 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" env: GH_TOKEN: ${{ github.token }} + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request || github.event.issue.pull_request + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -443,14 +507,14 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 env: GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} @@ -458,16 +522,11 @@ jobs: script: | const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: activation - path: /tmp/gh-aw - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + 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" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -479,15 +538,15 @@ 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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_ee26456e9d33cb21_EOF' + 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_ee26456e9d33cb21_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_05b64a640c1d5c26_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -641,62 +700,24 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); await main(); - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - # Mask immediately to prevent timing vulnerabilities - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${API_KEY}" - - PORT=3001 - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" - - name: Start MCP Gateway id: start-mcp-gateway env: + GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }} GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + 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 }} 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 }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" - + # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" - export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_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}" @@ -705,29 +726,24 @@ jobs: mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" export DEBUG="*" - + export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - case "${DOCKER_HOST:-}" in - unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; - /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; - * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; - esac - DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.22' - - mkdir -p /home/runner/.copilot + 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.1' + + 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_f1f5e8d8750acfbe_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_d97c92af15acf38e_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.1.0", + "container": "ghcr.io/github/github-mcp-server:v1.5.0", "env": { - "GITHUB_HOST": "\${GITHUB_SERVER_URL}", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_HOST": "${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" }, @@ -739,16 +755,35 @@ jobs: } }, "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + "type": "stdio", + "container": "ghcr.io/github/gh-aw-node", + "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], + "args": ["-w", "\${GITHUB_WORKSPACE}"], + "entrypoint": "sh", + "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"], + "env": { + "DEBUG": "*", + "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", + "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", + "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", + "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", + "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", + "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", + "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}", + "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", + "GITHUB_SHA": "\${GITHUB_SHA}", + "GITHUB_TOKEN": "\${GITHUB_TOKEN}", + "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", + "RUNNER_TEMP": "\${RUNNER_TEMP}" }, "guard-policies": { "write-sink": { "accept": [ "*" - ] + ], + "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} } } } @@ -760,7 +795,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_f1f5e8d8750acfbe_EOF + GH_AW_MCP_CONFIG_d97c92af15acf38e_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -789,41 +824,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + GH_AW_CHROOT_BINARIES_SOURCE_PATH="${RUNNER_TEMP}/gh-aw" GH_AW_CHROOT_IDENTITY_HOME="${RUNNER_TEMP}/gh-aw/home" node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs" fi GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --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 + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + 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_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 15 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -838,7 +883,8 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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: Detect agent errors if: always() id: detect-agent-errors @@ -846,17 +892,10 @@ jobs: run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Copy Copilot session state files to logs if: always() continue-on-error: true @@ -880,8 +919,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -915,6 +953,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + 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'); @@ -936,16 +975,7 @@ jobs: continue-on-error: true env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_firewall_logs.sh" --rootless - name: Parse token usage for step summary if: always() continue-on-error: true @@ -1006,17 +1036,19 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_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') runs-on: ubuntu-slim permissions: contents: read - discussions: write issues: write pull-requests: write concurrency: group: "gh-aw-conclusion-sdk-consistency-review" cancel-in-progress: false queue: max + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -1025,7 +1057,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1034,8 +1066,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1051,6 +1083,98 @@ jobs: 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 + id: download-safe-outputs-manifest + if: always() + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: safe-outputs-items + 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 + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: usage + path: | + /tmp/gh-aw/usage/aw_info.json + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.json + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/github_rate_limits.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + /tmp/gh-aw/usage/activity/summary.json + if-no-files-found: ignore + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache-conclusion + if: always() + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-sdkconsistencyreview-${{ github.run_id }} + restore-keys: agentic-workflow-usage-sdkconsistencyreview- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Write daily AIC usage cache entry + id: write-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + github-token: ${{ github.token }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context); + const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + await main(); + - name: Save daily AIC usage cache + id: save-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: agentic-workflow-usage-sdkconsistencyreview-${{ github.run_id }} + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Upload daily AIC usage cache artifact + id: upload-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: aic-usage-cache + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + if-no-files-found: ignore + retention-days: 7 - name: Process no-op messages id: noop uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1063,6 +1187,10 @@ jobs: 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_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_WORKFLOW_ID: "sdk-consistency-review" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1134,23 +1262,30 @@ jobs: GH_AW_WORKFLOW_ID: "sdk-consistency-review" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" GH_AW_ENGINE_ID: "copilot" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "15" - GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1163,19 +1298,22 @@ jobs: needs: - activation - agent - if: > - always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + if: always() && needs.agent.result != 'skipped' runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1184,8 +1322,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1203,7 +1341,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1212,7 +1350,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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04 ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3 - name: Check if detection needed id: detection_guard if: always() @@ -1231,12 +1369,13 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.copilot/mcp-config.json" rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + 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." @@ -1274,11 +1413,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.70 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.35 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1288,39 +1427,51 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap '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) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + 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.35/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*\"],\"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\"],\"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-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"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.35,squid=sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3,agent=sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed,agent-act=sha256:b00340a7b09c917c522cb806af6da1d12f2146e25a4a6198f1589b0116aee992,api-proxy=sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04,cli-proxy=sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5\"},\"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 - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + 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_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + _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:-/opt/hostedtoolcache}" + 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 - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + # 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 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + 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_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.82.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1334,7 +1485,21 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner + 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() + 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'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1384,18 +1549,22 @@ jobs: runs-on: ubuntu-slim permissions: contents: read - discussions: write issues: write pull-requests: write - timeout-minutes: 15 + timeout-minutes: 45 env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/sdk-consistency-review" GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.55" + GH_AW_ENGINE_VERSION: "1.0.70" + 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" GH_AW_WORKFLOW_ID: "sdk-consistency-review" GH_AW_WORKFLOW_NAME: "SDK Consistency Review Agent" @@ -1412,7 +1581,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1421,8 +1590,8 @@ 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.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.70" + GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1441,7 +1610,7 @@ jobs: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash - run: | + run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. GH_HOST="${GITHUB_SERVER_URL#https://}" @@ -1473,4 +1642,3 @@ jobs: /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - diff --git a/.github/workflows/sdk-consistency-review.md b/.github/workflows/sdk-consistency-review.md index 4111015ba5..28c0015228 100644 --- a/.github/workflows/sdk-consistency-review.md +++ b/.github/workflows/sdk-consistency-review.md @@ -24,6 +24,7 @@ permissions: contents: read pull-requests: read issues: read + copilot-requests: write tools: github: toolsets: [default] diff --git a/.github/workflows/verify-compiled.yml b/.github/workflows/verify-compiled.yml index 2e3eee554c..946f6f903f 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@main + uses: github/gh-aw-actions/setup-cli@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 with: - version: v0.77.5 + version: v0.82.10 - name: Recompile workflows run: gh aw compile - name: Check for uncommitted changes diff --git a/.gitignore b/.gitignore index 1485d3a9c4..4aff9be11d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ docs/.validation/ # Visual Studio .vs/ +# Intellij IDEA +.idea/ + # C# Dev Kit *.csproj.lscache diff --git a/CHANGELOG.md b/CHANGELOG.md index acf014c538..41bf05e171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,83 @@ All notable changes to the Copilot SDK are documented in this file. This changelog is automatically generated by an AI agent when stable releases are published. See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the full list. +## [v1.0.7](https://github.com/github/copilot-sdk/releases/tag/v1.0.7) (2026-07-16) + +### Feature: in-process (FFI) transport + +The SDK can now host the Copilot runtime in-process by loading the native runtime library via its C ABI (FFI), eliminating the overhead of spawning a child process. This experimental transport is available for Node.js, Rust, Python, and Go. ([#1953](https://github.com/github/copilot-sdk/pull/1953), [#1915](https://github.com/github/copilot-sdk/pull/1915), [#1975](https://github.com/github/copilot-sdk/pull/1975), [#1976](https://github.com/github/copilot-sdk/pull/1976)) + +```ts +const client = new CopilotClient({ connection: RuntimeConnection.forInProcess() }); +``` + +```cs +var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForInProcess() }); +``` + +### Feature: tool search configuration + +A new `toolSearch` session option controls how the SDK defers tools when the total tool count exceeds a threshold. When enabled (the default), excess MCP and external tools are surfaced on demand through the built-in `tool_search_tool` rather than pre-loaded into every prompt. Tool results can also include `toolReferences` to link cited sources back to the tool that produced them. ([#1933](https://github.com/github/copilot-sdk/pull/1933)) + +```ts +const session = await client.createSession({ + toolSearch: { defer: "auto" }, +}); +``` + +```cs +var session = await client.CreateSessionAsync(new SessionConfig +{ + ToolSearch = new ToolSearchConfig { Defer = "auto" }, +}); +``` + +### Feature: opaque metadata passthrough on tool definitions + +Tool definitions now accept an optional `metadata` bag that is forwarded verbatim in `session.create` and `session.resume` RPC calls. This lets hosts attach namespaced, implementation-specific metadata to tools without expanding the typed public contract; unknown keys are preserved and round-tripped untouched. ([#1864](https://github.com/github/copilot-sdk/pull/1864)) + +```ts +session.defineTool("my-tool", { metadata: { "myapp:priority": 1 } }, handler); +``` + +```cs +session.DefineTool("my-tool", new ToolOptions { Metadata = new() { ["myapp:priority"] = 1 } }, handler); +``` + +### Other changes + +- feature: **[All SDKs]** add `canvasProvider` field to session create/resume config so hosts can supply a stable canvas-provider identity that survives cold resume ([#1847](https://github.com/github/copilot-sdk/pull/1847)) +- feature: **[All SDKs]** forward `enableManagedSettings` flag in session create/resume for enterprise managed-settings enforcement ([#1925](https://github.com/github/copilot-sdk/pull/1925)) +- feature: **[All SDKs]** propagate `agentId`, `parentAgentId`, and `interactionType` from LLM inference start frames into request-handler contexts ([#1949](https://github.com/github/copilot-sdk/pull/1949)) +- improvement: **[Rust]** make tool schema and MCP server serialization deterministic by replacing `HashMap` with `IndexMap` ([#1931](https://github.com/github/copilot-sdk/pull/1931)) +- improvement: **[Rust]** use `native-tls` for the build-time CLI download ([#1964](https://github.com/github/copilot-sdk/pull/1964)) +- bugfix: **[.NET]** avoid Windows in-process test teardown deadlock ([#1997](https://github.com/github/copilot-sdk/pull/1997)) + +### New contributors + +- @agoncal made their first contribution in [#1951](https://github.com/github/copilot-sdk/pull/1951) +- @Shivam60 made their first contribution in [#1964](https://github.com/github/copilot-sdk/pull/1964) +- @rinceyuan made their first contribution in [#1978](https://github.com/github/copilot-sdk/pull/1978) +- @belaltaher8 made their first contribution in [#1864](https://github.com/github/copilot-sdk/pull/1864) + +## [java/v1.0.6](https://github.com/github/copilot-sdk/releases/tag/java/v1.0.6) (2026-07-08) + +### Feature: inline lambda tool definitions + +Developers can now define tools directly at the call site using `ToolDefinition.from(...)` with typed lambda handlers and `Param.of(...)` parameter metadata — no separate annotated class required. Async variants (`fromAsync`) and `ToolInvocation` context injection (`fromWithToolInvocation`) are also available. ([#1895](https://github.com/github/copilot-sdk/pull/1895)) + +```java +ToolDefinition greet = ToolDefinition.from( + "greet", "Greets a user by name", + Param.of(String.class, "name", "The user's name"), + name -> "Hello, " + name + "!"); +``` + +### Other changes + +- bugfix: **[Java]** preserve explicit null map values in JSON-RPC params so user setting clears reach the CLI ([#1906](https://github.com/github/copilot-sdk/pull/1906)) +- feature: **[Java]** add experimental `onGitHubTelemetry` callback on `CopilotClientOptions` for receiving forwarded GitHub telemetry events ([#1835](https://github.com/github/copilot-sdk/pull/1835)) + ## [java/v1.0.5-01](https://github.com/github/copilot-sdk/releases/tag/java/v1.0.5-01) (2026-07-01) ### Feature: new session options — citations, agent exclusions, and credit limits diff --git a/docs/README.md b/docs/README.md index 2533d74c88..9e0d8fddff 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ -# Copilot SDK +# Copilot SDK Welcome to the GitHub Copilot SDK docs. Whether you're building your first Copilot-powered app or deploying to production, you'll find what you need here. diff --git a/docs/auth/README.md b/docs/auth/README.md index b09646d5d7..282bcb0192 100644 --- a/docs/auth/README.md +++ b/docs/auth/README.md @@ -7,6 +7,6 @@ Choose the authentication method that best fits your deployment scenario for the ## Authentication priority -When multiple credentials are configured, an explicit SDK token takes priority, followed by HMAC or direct Copilot API environment authentication, environment variable GitHub tokens, stored Copilot CLI credentials, and then GitHub CLI credentials. See [Authenticate Copilot SDK](authenticate.md#authentication-priority) for details. +When multiple credentials are configured, an explicit SDK token takes priority, followed by direct Copilot API environment authentication, environment variable GitHub tokens, stored Copilot CLI credentials, and then GitHub CLI credentials. See [Authenticate Copilot SDK](authenticate.md#authentication-priority) for details. For multi-user server mode, pass a per-session `gitHubToken` so each session runs with the correct GitHub identity; see [Multi-user and server deployments](../setup/multi-tenancy.md). diff --git a/docs/auth/authenticate.md b/docs/auth/authenticate.md index 0c4d706992..1a01901863 100644 --- a/docs/auth/authenticate.md +++ b/docs/auth/authenticate.md @@ -9,7 +9,7 @@ The GitHub Copilot SDK supports multiple authentication methods to fit different | [GitHub Signed-in User](#github-signed-in-user) | Interactive apps where users sign in with GitHub | Yes | | [OAuth GitHub App](#oauth-github-app) | Apps acting on behalf of users via OAuth | Yes | | [Environment Variables](#environment-variables) | CI/CD, automation, server-to-server | Yes | -| [BYOK (Bring Your Own Key)](./byok.md) | Using your own API keys (Azure AI Foundry, OpenAI, etc.) | No | +| [BYOK (Bring Your Own Key)](./byok.md) | Using your own API keys (Azure AI Foundry, OpenAI, and more) | No | ## GitHub signed-in user @@ -221,7 +221,7 @@ client.start().get(); **Supported token types:** * `gho_` - OAuth user access tokens -* `ghu_` - GitHub App user access tokens +* `ghu_` - GitHub App user access tokens * `github_pat_` - Fine-grained personal access tokens **Not supported:** @@ -275,7 +275,7 @@ await client.start() **When to use:** -* CI/CD pipelines (GitHub Actions, Jenkins, etc.) +* CI/CD pipelines (GitHub Actions, Jenkins, and more) * Automated testing * Server-side applications with service accounts * Development when you don't want to use interactive login @@ -301,7 +301,6 @@ BYOK allows you to use your own API keys from model providers like Azure AI Foun When multiple authentication methods are available, the SDK uses them in this priority order: 1. **Explicit `gitHubToken`** - Token passed directly to the SDK client or session configuration -1. **HMAC key** - `CAPI_HMAC_KEY` or `COPILOT_HMAC_KEY` environment variables 1. **Direct API token** - `GITHUB_COPILOT_API_TOKEN` with `COPILOT_API_URL` 1. **Environment variable tokens** - `COPILOT_GITHUB_TOKEN` → `GH_TOKEN` → `GITHUB_TOKEN` 1. **Stored OAuth credentials** - From previous `copilot` CLI login diff --git a/docs/auth/byok.md b/docs/auth/byok.md index 1bf3646640..01d954a40b 100644 --- a/docs/auth/byok.md +++ b/docs/auth/byok.md @@ -529,7 +529,7 @@ import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient(); const session = await client.createSession({ - model: "gpt-4.1", + model: "gpt-5.4", provider: { type: "azure", baseUrl: "https://my-resource.openai.azure.com", @@ -560,7 +560,7 @@ import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient(); const session = await client.createSession({ - model: "gpt-4.1", + model: "gpt-5.4", provider: { type: "openai", baseUrl: "https://your-resource.openai.azure.com/openai/v1/", diff --git a/docs/features/cloud-sessions.md b/docs/features/cloud-sessions.md index 0d670b996f..863f9456b9 100644 --- a/docs/features/cloud-sessions.md +++ b/docs/features/cloud-sessions.md @@ -235,7 +235,7 @@ Capture the URL by subscribing to `session.info` and filtering by `infoType: "re session.on("session.info", (event) => { if (event.data?.infoType === "remote" && event.data.url) { console.log("Open from web or mobile:", event.data.url); - // e.g. surface in your UI as a shareable link or QR code. + // For example, surface in your UI as a shareable link or QR code. } }); ``` diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md index fb2f81fd1c..3cab77474e 100644 --- a/docs/features/custom-agents.md +++ b/docs/features/custom-agents.md @@ -37,7 +37,7 @@ const client = new CopilotClient(); await client.start(); const session = await client.createSession({ - model: "gpt-4.1", + model: "gpt-5.4", customAgents: [ { name: "researcher", @@ -71,7 +71,7 @@ await client.start() session = await client.create_session( on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), - model="gpt-4.1", + model="gpt-5.4", custom_agents=[ { "name": "researcher", @@ -112,7 +112,7 @@ func main() { client.Start(ctx) session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-4.1", + Model: "gpt-5.4", CustomAgents: []copilot.CustomAgentConfig{ { Name: "researcher", @@ -144,7 +144,7 @@ client := copilot.NewClient(nil) client.Start(ctx) session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-4.1", + Model: "gpt-5.4", CustomAgents: []copilot.CustomAgentConfig{ { Name: "researcher", @@ -179,7 +179,7 @@ using GitHub.Copilot.Rpc; await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig { - Model = "gpt-4.1", + Model = "gpt-5.4", CustomAgents = new List { new() @@ -219,7 +219,7 @@ try (var client = new CopilotClient()) { var session = client.createSession( new SessionConfig() - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setCustomAgents(List.of( new CustomAgentConfig() .setName("researcher") @@ -253,10 +253,14 @@ try (var client = new CopilotClient()) { | `mcpServers` | `object` | | MCP server configurations specific to this agent | | `infer` | `boolean` | | Whether the runtime can auto-select this agent (default: `true`) | | `skills` | `string[]` | | Skill names to preload into the agent's context at startup | +| `model` | `string` | | Model identifier to use while this agent runs | +| `reasoningEffort` | `string` | | Reasoning effort to use while this agent runs. When omitted, no override is sent and the backend chooses its default | > [!TIP] > A good `description` helps the runtime match user intent to the right agent. Be specific about the agent's expertise and capabilities. +Set `model` and `reasoningEffort` to override the parent session's model settings while a custom agent runs. When `reasoningEffort` is omitted, the SDK sends no per-agent override and the backend chooses its default. The parent session effort is not inherited, and the SDK does not add a per-agent default. Python uses `reasoning_effort`, .NET uses `ReasoningEffort`, Go uses `ReasoningEffort`, Java uses `setReasoningEffort`, and Rust uses `with_reasoning_effort`. + In addition to per-agent configuration above, you can set `agent` on the **session config** itself to pre-select which custom agent is active when the session starts. See [Selecting an Agent at Session Creation](#selecting-an-agent-at-session-creation) below. | Session Config Property | Type | Description | @@ -529,7 +533,7 @@ func main() { client.Start(ctx) session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-4.1", + Model: "gpt-5.4", OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { return &rpc.PermissionDecisionApproveOnce{}, nil }, diff --git a/docs/features/fleet-mode.md b/docs/features/fleet-mode.md index a830f4931d..cbb737b78d 100644 --- a/docs/features/fleet-mode.md +++ b/docs/features/fleet-mode.md @@ -8,18 +8,18 @@ Fleet mode is useful when the work can be decomposed before execution and each u Good fits include: -- Multi-file refactors where each worker owns a file, package, or language SDK. -- Batch reviews where each worker checks a separate diff, module, or alert group. -- Parallel research across independent repositories, services, or feature areas. -- Documentation refreshes where each worker owns a page or topic. -- Migration tasks where each worker can validate its own slice and report back. +* Multi-file refactors where each worker owns a file, package, or language SDK. +* Batch reviews where each worker checks a separate diff, module, or alert group. +* Parallel research across independent repositories, services, or feature areas. +* Documentation refreshes where each worker owns a page or topic. +* Migration tasks where each worker can validate its own slice and report back. Avoid fleet mode for: -- Sequential tasks where step 2 needs the concrete output from step 1. -- Tightly coupled edits where workers would contend for the same files. -- Small tasks that one synchronous sub-agent or the parent agent can finish quickly. -- Tasks that require continuous shared reasoning rather than clear ownership. +* Sequential tasks where step 2 needs the concrete output from step 1. +* Tightly coupled edits where workers would contend for the same files. +* Small tasks that one synchronous sub-agent or the parent agent can finish quickly. +* Tasks that require continuous shared reasoning rather than clear ownership. Fleet mode works best when the parent session can create clear units of work, assign one owner per unit, and define what each worker must return. @@ -321,29 +321,29 @@ Keep plugin-provided sub-agent types narrow and descriptive so the orchestrator ## Best practices -- Decompose the work into independent units before starting fleet mode. -- Minimize dependencies between todos; dependencies reduce parallelism. -- Give each todo a durable ID, a clear title, and a complete description. -- Make each sub-agent own exactly one todo at a time. -- Use background sub-agents for truly parallel work. -- Use synchronous sub-agent calls for serialized steps or validation gates. -- Provide each sub-agent with complete context; sub-agents are stateless across calls. -- Include file paths, commands, expected outputs, and constraints in each worker prompt. -- Do not dispatch a single background sub-agent; prefer a synchronous call or batch multiple workers in parallel. -- Avoid assigning overlapping files to different workers unless the parent agent will reconcile conflicts explicitly. -- Require every worker to report what it changed, how it validated the change, and what remains blocked. -- Have the parent agent verify the combined result after workers finish. +* Decompose the work into independent units before starting fleet mode. +* Minimize dependencies between todos; dependencies reduce parallelism. +* Give each todo a durable ID, a clear title, and a complete description. +* Make each sub-agent own exactly one todo at a time. +* Use background sub-agents for truly parallel work. +* Use synchronous sub-agent calls for serialized steps or validation gates. +* Provide each sub-agent with complete context; sub-agents are stateless across calls. +* Include file paths, commands, expected outputs, and constraints in each worker prompt. +* Do not dispatch a single background sub-agent; prefer a synchronous call or batch multiple workers in parallel. +* Avoid assigning overlapping files to different workers unless the parent agent will reconcile conflicts explicitly. +* Require every worker to report what it changed, how it validated the change, and what remains blocked. +* Have the parent agent verify the combined result after workers finish. ## Limitations and open questions -- Fleet mode is exposed through generated session RPC bindings and is marked experimental in several SDKs. -- The SQL todos pattern is the canonical coordination model in the runtime guidance, but whether it is a stable extensibility contract for SDK consumers is still an open question. -- `subagentStart` and `subagentStop` are runtime hook names; this branch exposes sub-agent lifecycle to SDK consumers through the generic session event stream, not dedicated hook callbacks. -- Plugin sub-agent registration is configured at the runtime layer through `--plugin-dir`; no SDK-level plugin registration helper was verified on this branch. -- Java native typed bindings for `session.fleet.start` were not found in the Java SDK source on this branch. -- Fleet mode does not remove the need for parent-agent review. Parallel workers can produce inconsistent assumptions that the orchestrator must reconcile. +* Fleet mode is exposed through generated session RPC bindings and is marked experimental in several SDKs. +* The SQL todos pattern is the canonical coordination model in the runtime guidance, but whether it is a stable extensibility contract for SDK consumers is still an open question. +* `subagentStart` and `subagentStop` are runtime hook names; this branch exposes sub-agent lifecycle to SDK consumers through the generic session event stream, not dedicated hook callbacks. +* Plugin sub-agent registration is configured at the runtime layer through `--plugin-dir`; no SDK-level plugin registration helper was verified on this branch. +* Java native typed bindings for `session.fleet.start` were not found in the Java SDK source on this branch. +* Fleet mode does not remove the need for parent-agent review. Parallel workers can produce inconsistent assumptions that the orchestrator must reconcile. ## See also -- [Custom agents and sub-agent orchestration](custom-agents.md) -- [Hooks](hooks.md) +* [Custom agents and sub-agent orchestration](custom-agents.md) +* [Hooks](hooks.md) diff --git a/docs/features/hooks.md b/docs/features/hooks.md index 6af5232a68..feee55546d 100644 --- a/docs/features/hooks.md +++ b/docs/features/hooks.md @@ -1051,16 +1051,16 @@ const session = await client.createSession({ For full type definitions, input/output field tables, and additional examples for every hook, see the API reference: -- [Hooks Overview](../hooks/hooks-overview.md) -- [Pre-Tool Use](../hooks/pre-tool-use.md) -- [Post-Tool Use](../hooks/post-tool-use.md) -- [User Prompt Submitted](../hooks/user-prompt-submitted.md) -- [Session Lifecycle](../hooks/session-lifecycle.md) -- [Error Handling](../hooks/error-handling.md) +* [Hooks Overview](../hooks/hooks-overview.md) +* [Pre-Tool Use](../hooks/pre-tool-use.md) +* [Post-Tool Use](../hooks/post-tool-use.md) +* [User Prompt Submitted](../hooks/user-prompt-submitted.md) +* [Session Lifecycle](../hooks/session-lifecycle.md) +* [Error Handling](../hooks/error-handling.md) ## See also -- [Getting Started](../getting-started.md) -- [Custom Agents & Sub-Agent Orchestration](./custom-agents.md) -- [Streaming Session Events](./streaming-events.md) -- [Debugging Guide](../troubleshooting/debugging.md) \ No newline at end of file +* [Getting Started](../getting-started.md) +* [Custom Agents & Sub-Agent Orchestration](./custom-agents.md) +* [Streaming Session Events](./streaming-events.md) +* [Debugging Guide](../troubleshooting/debugging.md) \ No newline at end of file diff --git a/docs/features/image-input.md b/docs/features/image-input.md index c63a80c11d..321e5d2fc6 100644 --- a/docs/features/image-input.md +++ b/docs/features/image-input.md @@ -47,7 +47,7 @@ const client = new CopilotClient(); await client.start(); const session = await client.createSession({ - model: "gpt-4.1", + model: "gpt-5.4", onPermissionRequest: async () => ({ kind: "approve-once" }), }); @@ -75,7 +75,7 @@ await client.start() session = await client.create_session( on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), - model="gpt-4.1", + model="gpt-5.4", ) await session.send( @@ -110,7 +110,7 @@ func main() { client.Start(ctx) session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-4.1", + Model: "gpt-5.4", OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { return &rpc.PermissionDecisionApproveOnce{}, nil }, @@ -136,7 +136,7 @@ client := copilot.NewClient(nil) client.Start(ctx) session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-4.1", + Model: "gpt-5.4", OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { return &rpc.PermissionDecisionApproveOnce{}, nil }, @@ -171,7 +171,7 @@ public static class ImageInputExample await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig { - Model = "gpt-4.1", + Model = "gpt-5.4", OnPermissionRequest = (req, inv) => Task.FromResult(PermissionDecision.ApproveOnce()), }); @@ -200,7 +200,7 @@ using GitHub.Copilot.Rpc; await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig { - Model = "gpt-4.1", + Model = "gpt-5.4", OnPermissionRequest = (req, inv) => Task.FromResult(PermissionDecision.ApproveOnce()), }); @@ -234,7 +234,7 @@ try (var client = new CopilotClient()) { var session = client.createSession( new SessionConfig() - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); @@ -263,7 +263,7 @@ const client = new CopilotClient(); await client.start(); const session = await client.createSession({ - model: "gpt-4.1", + model: "gpt-5.4", onPermissionRequest: async () => ({ kind: "approve-once" }), }); @@ -294,7 +294,7 @@ await client.start() session = await client.create_session( on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), - model="gpt-4.1", + model="gpt-5.4", ) base64_image_data = "..." # your base64-encoded image @@ -332,7 +332,7 @@ func main() { client.Start(ctx) session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-4.1", + Model: "gpt-5.4", OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { return &rpc.PermissionDecisionApproveOnce{}, nil }, @@ -387,7 +387,7 @@ public static class BlobAttachmentExample await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig { - Model = "gpt-4.1", + Model = "gpt-5.4", OnPermissionRequest = (req, inv) => Task.FromResult(PermissionDecision.ApproveOnce()), }); @@ -442,7 +442,7 @@ try (var client = new CopilotClient()) { var session = client.createSession( new SessionConfig() - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); diff --git a/docs/features/skills.md b/docs/features/skills.md index 6db955e743..5b8388162a 100644 --- a/docs/features/skills.md +++ b/docs/features/skills.md @@ -24,7 +24,7 @@ import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient(); const session = await client.createSession({ - model: "gpt-4.1", + model: "gpt-5.4", skillDirectories: [ "./skills/code-review", "./skills/documentation", @@ -50,7 +50,7 @@ async def main(): session = await client.create_session( on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), - model="gpt-4.1", + model="gpt-5.4", skill_directories=[ "./skills/code-review", "./skills/documentation", @@ -87,7 +87,7 @@ func main() { defer client.Stop() session, err := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-4.1", + Model: "gpt-5.4", SkillDirectories: []string{ "./skills/code-review", "./skills/documentation", @@ -122,7 +122,7 @@ using GitHub.Copilot.Rpc; await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig { - Model = "gpt-4.1", + Model = "gpt-5.4", SkillDirectories = new List { "./skills/code-review", @@ -154,7 +154,7 @@ try (var client = new CopilotClient()) { var session = client.createSession( new SessionConfig() - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setSkillDirectories(List.of( "./skills/code-review", "./skills/documentation" diff --git a/docs/features/steering-and-queueing.md b/docs/features/steering-and-queueing.md index 7dbdc17b7c..7bfffc433d 100644 --- a/docs/features/steering-and-queueing.md +++ b/docs/features/steering-and-queueing.md @@ -47,7 +47,7 @@ const client = new CopilotClient(); await client.start(); const session = await client.createSession({ - model: "gpt-4.1", + model: "gpt-5.4", onPermissionRequest: async () => ({ kind: "approve-once" }), }); @@ -77,7 +77,7 @@ async def main(): session = await client.create_session( on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), - model="gpt-4.1", + model="gpt-5.4", ) # Start a long-running task @@ -118,7 +118,7 @@ func main() { defer client.Stop() session, err := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-4.1", + Model: "gpt-5.4", OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { return &rpc.PermissionDecisionApproveOnce{}, nil }, @@ -158,7 +158,7 @@ using GitHub.Copilot.Rpc; await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig { - Model = "gpt-4.1", + Model = "gpt-5.4", OnPermissionRequest = (req, inv) => Task.FromResult(PermissionDecision.ApproveOnce()), }); @@ -191,7 +191,7 @@ try (var client = new CopilotClient()) { var session = client.createSession( new SessionConfig() - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); @@ -234,7 +234,7 @@ const client = new CopilotClient(); await client.start(); const session = await client.createSession({ - model: "gpt-4.1", + model: "gpt-5.4", onPermissionRequest: async () => ({ kind: "approve-once" }), }); @@ -269,7 +269,7 @@ async def main(): session = await client.create_session( on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), - model="gpt-4.1", + model="gpt-5.4", ) # Send an initial task @@ -311,7 +311,7 @@ func main() { client.Start(ctx) session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-4.1", + Model: "gpt-5.4", OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { return &rpc.PermissionDecisionApproveOnce{}, nil }, @@ -371,7 +371,7 @@ public static class QueueingExample await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig { - Model = "gpt-4.1", + Model = "gpt-5.4", OnPermissionRequest = (req, inv) => Task.FromResult(PermissionDecision.ApproveOnce()), }); @@ -434,7 +434,7 @@ try (var client = new CopilotClient()) { var session = client.createSession( new SessionConfig() - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); @@ -475,7 +475,7 @@ You can use both patterns together in a single session. Steering affects the cur ```typescript const session = await client.createSession({ - model: "gpt-4.1", + model: "gpt-5.4", onPermissionRequest: async () => ({ kind: "approve-once" }), }); @@ -503,7 +503,7 @@ await session.send({ ```python session = await client.create_session( on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), - model="gpt-4.1", + model="gpt-5.4", ) # Start a task diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 3703f871d3..0c2dafdefb 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -130,7 +130,7 @@ func main() { client := copilot.NewClient(nil) session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-4.1", + Model: "gpt-5.4", Streaming: copilot.Bool(true), OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { return &rpc.PermissionDecisionApproveOnce{}, nil @@ -306,7 +306,7 @@ Ephemeral. Token usage and cost information for an individual API call. | Data Field | Type | Required | Description | |------------|------|----------|-------------| -| `model` | `string` | ✅ | Model identifier (e.g., `"gpt-4.1"`) | +| `model` | `string` | ✅ | Model identifier (e.g., `"gpt-5.4"`) | | `inputTokens` | `number` | | Input tokens consumed | | `outputTokens` | `number` | | Output tokens produced | | `cacheReadTokens` | `number` | | Tokens read from prompt cache | diff --git a/docs/getting-started.md b/docs/getting-started.md index c258c50e9c..b14fb73e52 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -150,7 +150,7 @@ Create `index.ts`: import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient(); -const session = await client.createSession({ model: "gpt-4.1" }); +const session = await client.createSession({ model: "auto" }); const response = await session.sendAndWait({ prompt: "What is 2 + 2?" }); console.log(response?.data.content); @@ -181,7 +181,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1") + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="auto") response = await session.send_and_wait("What is 2 + 2?") print(response.data.content) @@ -223,7 +223,7 @@ func main() { } defer client.Stop() - session, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-4.1"}) + session, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "auto"}) if err != nil { log.Fatal(err) } @@ -304,7 +304,7 @@ using GitHub.Copilot; await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig { - Model = "gpt-4.1", + Model = "auto", OnPermissionRequest = PermissionHandler.ApproveAll }); @@ -337,7 +337,7 @@ public class HelloCopilot { var session = client.createSession( new SessionConfig() - .setModel("gpt-4.1") + .setModel("auto") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); @@ -383,7 +383,7 @@ import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient(); const session = await client.createSession({ - model: "gpt-4.1", + model: "auto", streaming: true, }); @@ -419,7 +419,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", streaming=True) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="auto", streaming=True) # Listen for response chunks def handle_event(event): @@ -466,7 +466,7 @@ func main() { defer client.Stop() session, err := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-4.1", + Model: "auto", Streaming: copilot.Bool(true), }) if err != nil { @@ -562,7 +562,7 @@ using GitHub.Copilot; await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig { - Model = "gpt-4.1", + Model = "auto", OnPermissionRequest = PermissionHandler.ApproveAll, Streaming = true, }); @@ -602,7 +602,7 @@ public class HelloCopilot { var session = client.createSession( new SessionConfig() - .setModel("gpt-4.1") + .setModel("auto") .setStreaming(true) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); @@ -912,7 +912,7 @@ const getWeather = defineTool("get_weather", { const client = new CopilotClient(); const session = await client.createSession({ - model: "gpt-4.1", + model: "auto", streaming: true, tools: [getWeather], }); @@ -968,7 +968,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", streaming=True, tools=[get_weather]) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="auto", streaming=True, tools=[get_weather]) def handle_event(event): if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: @@ -1045,7 +1045,7 @@ func main() { defer client.Stop() session, err := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-4.1", + Model: "auto", Streaming: copilot.Bool(true), Tools: []copilot.Tool{getWeather}, }) @@ -1185,7 +1185,7 @@ var getWeather = CopilotTool.DefineTool( await using var session = await client.CreateSessionAsync(new SessionConfig { - Model = "gpt-4.1", + Model = "auto", OnPermissionRequest = PermissionHandler.ApproveAll, Streaming = true, Tools = [getWeather], @@ -1259,7 +1259,7 @@ public class HelloCopilot { var session = client.createSession( new SessionConfig() - .setModel("gpt-4.1") + .setModel("auto") .setStreaming(true) .setTools(List.of(getWeather)) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) @@ -1316,7 +1316,7 @@ const getWeather = defineTool("get_weather", { const client = new CopilotClient(); const session = await client.createSession({ - model: "gpt-4.1", + model: "auto", streaming: true, tools: [getWeather], }); @@ -1389,7 +1389,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", streaming=True, tools=[get_weather]) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="auto", streaming=True, tools=[get_weather]) def handle_event(event): if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: @@ -1482,7 +1482,7 @@ func main() { defer client.Stop() session, err := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-4.1", + Model: "auto", Streaming: copilot.Bool(true), Tools: []copilot.Tool{getWeather}, }) @@ -1671,7 +1671,7 @@ var getWeather = CopilotTool.DefineTool( await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig { - Model = "gpt-4.1", + Model = "auto", OnPermissionRequest = PermissionHandler.ApproveAll, Streaming = true, Tools = [getWeather] @@ -1765,7 +1765,7 @@ public class WeatherAssistant { var session = client.createSession( new SessionConfig() - .setModel("gpt-4.1") + .setModel("auto") .setStreaming(true) .setOnPermissionRequest(request -> CompletableFuture.completedFuture(PermissionDecision.allow()) diff --git a/docs/hooks/post-tool-use.md b/docs/hooks/post-tool-use.md index 45964b0344..b7ef3af1c4 100644 --- a/docs/hooks/post-tool-use.md +++ b/docs/hooks/post-tool-use.md @@ -2,10 +2,10 @@ The `onPostToolUse` hook is called **after** a tool executes **successfully**. Use it to: -- Transform or filter tool results -- Log tool execution for auditing -- Add context based on results -- Suppress results from the conversation +* Transform or filter tool results +* Log tool execution for auditing +* Add context based on results +* Suppress results from the conversation > **Failure variant** — `onPostToolUse` only fires for successful tool executions. To observe **failed** tool calls, register `onPostToolUseFailure` (`on_post_tool_use_failure` in Python, `OnPostToolUseFailure` in Go/.NET, `on_post_tool_use_failure` in Rust). The handler receives `{ sessionId, toolName, toolArgs, error, timestamp, workingDirectory }` — the `error` field is a string extracted from the tool's failure result — and may return `{ additionalContext: string }` to inject extra guidance for the model (e.g. retry hints). See the [hooks overview](./hooks-overview.md) for the full list. > @@ -507,6 +507,6 @@ const session = await client.createSession({ ## See also -- [Hooks Overview](./README.md) -- [Pre-Tool Use Hook](./pre-tool-use.md) -- [Error Handling Hook](./error-handling.md) \ No newline at end of file +* [Hooks Overview](./README.md) +* [Pre-Tool Use Hook](./pre-tool-use.md) +* [Error Handling Hook](./error-handling.md) \ No newline at end of file diff --git a/docs/integrations/microsoft-agent-framework.md b/docs/integrations/microsoft-agent-framework.md index 3d6d990860..663a20a799 100644 --- a/docs/integrations/microsoft-agent-framework.md +++ b/docs/integrations/microsoft-agent-framework.md @@ -123,7 +123,7 @@ var client = new CopilotClient(); client.start().get(); var session = client.createSession(new SessionConfig() - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); @@ -217,7 +217,7 @@ const getWeather = DefineTool({ const client = new CopilotClient(); const session = await client.createSession({ - model: "gpt-4.1", + model: "gpt-5.4", tools: [getWeather], onPermissionRequest: async () => ({ kind: "approve-once" }), }); @@ -255,7 +255,7 @@ try (var client = new CopilotClient()) { client.start().get(); var session = client.createSession(new SessionConfig() - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setTools(List.of(getWeather)) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); @@ -296,7 +296,7 @@ AIAgent reviewer = copilotClient.AsAIAgent(new AIAgentOptions // Azure OpenAI agent for generating documentation AIAgent documentor = AIAgent.FromOpenAI(new OpenAIAgentOptions { - Model = "gpt-4.1", + Model = "gpt-5.4", Instructions = "You write clear, concise documentation for code changes.", }); @@ -330,7 +330,7 @@ async def main(): # OpenAI agent for documentation documentor = OpenAIAgent( - model="gpt-4.1", + model="gpt-5.4", instructions="You write clear, concise documentation for code changes.", ) @@ -360,7 +360,7 @@ client.start().get(); // Step 1: Code review session var reviewer = client.createSession(new SessionConfig() - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); @@ -370,7 +370,7 @@ var review = reviewer.sendAndWait(new MessageOptions() // Step 2: Documentation session using review output var documentor = client.createSession(new SessionConfig() - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); @@ -434,12 +434,12 @@ var client = new CopilotClient(); client.start().get(); var securitySession = client.createSession(new SessionConfig() - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); var perfSession = client.createSession(new SessionConfig() - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); @@ -518,7 +518,7 @@ import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient(); const session = await client.createSession({ - model: "gpt-4.1", + model: "gpt-5.4", streaming: true, onPermissionRequest: async () => ({ kind: "approve-once" }), }); @@ -544,7 +544,7 @@ var client = new CopilotClient(); client.start().get(); var session = client.createSession(new SessionConfig() - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setStreaming(true) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); @@ -598,7 +598,7 @@ import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient(); const session = await client.createSession({ - model: "gpt-4.1", + model: "gpt-5.4", onPermissionRequest: async () => ({ kind: "approve-once" }), }); const response = await session.sendAndWait({ prompt: "Explain this code" }); diff --git a/docs/setup/azure-managed-identity.md b/docs/setup/azure-managed-identity.md index 51ca0dc0be..8afac6e1d3 100644 --- a/docs/setup/azure-managed-identity.md +++ b/docs/setup/azure-managed-identity.md @@ -240,7 +240,7 @@ class ManagedIdentityCopilotAgent: | Variable | Description | Example | |----------|-------------|---------| -| `AZURE_TOKEN_CREDENTIALS` | When running in **Azure**, set it to `ManagedIdentityCredential`. When running **locally**, set it to either `dev` or a developer tool credential name, such as `AzureCliCredential`. | | +| `AZURE_TOKEN_CREDENTIALS` | When running in **Azure**, set it to `ManagedIdentityCredential`. When running **locally**, set it to either `dev` or a developer tool credential name, such as `AzureCliCredential`. | `ManagedIdentityCredential` | | `FOUNDRY_RESOURCE_URL` | Your Microsoft Foundry resource URL | `https://.openai.azure.com` | No API key environment variable is needed—authentication is handled by `DefaultAzureCredential`, which automatically supports: diff --git a/docs/setup/backend-services.md b/docs/setup/backend-services.md index ca4a310b6f..7f1da36e82 100644 --- a/docs/setup/backend-services.md +++ b/docs/setup/backend-services.md @@ -134,7 +134,7 @@ const client = new CopilotClient({ const session = await client.createSession({ sessionId: `user-${userId}-${Date.now()}`, - model: "gpt-4.1", + model: "gpt-5.4", availableTools: ["custom:*"], gitHubToken: user.githubToken, }); @@ -157,7 +157,7 @@ client = CopilotClient( ) await client.start() -session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", session_id=f"user-{user_id}-{int(time.time())}") +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5.4", session_id=f"user-{user_id}-{int(time.time())}") response = await session.send_and_wait(message) ``` @@ -191,7 +191,7 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ SessionID: fmt.Sprintf("user-%s-%d", userID, time.Now().Unix()), - Model: "gpt-4.1", + Model: "gpt-5.4", }) response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: message}) @@ -209,7 +209,7 @@ defer client.Stop() session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ SessionID: fmt.Sprintf("user-%s-%d", userID, time.Now().Unix()), - Model: "gpt-4.1", + Model: "gpt-5.4", }) response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: message}) @@ -235,7 +235,7 @@ var client = new CopilotClient(new CopilotClientOptions await using var session = await client.CreateSessionAsync(new SessionConfig { SessionId = $"user-{userId}-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}", - Model = "gpt-4.1", + Model = "gpt-5.4", }); var response = await session.SendAndWaitAsync( @@ -252,7 +252,7 @@ var client = new CopilotClient(new CopilotClientOptions await using var session = await client.CreateSessionAsync(new SessionConfig { SessionId = $"user-{userId}-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}", - Model = "gpt-4.1", + Model = "gpt-5.4", }); var response = await session.SendAndWaitAsync( @@ -280,7 +280,7 @@ try { var session = client.createSession(new SessionConfig() .setSessionId(String.format("user-%s-%d", userId, System.currentTimeMillis() / 1000)) - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); @@ -332,7 +332,7 @@ const client = new CopilotClient({ app.post("/chat", authMiddleware, async (req, res) => { const session = await client.createSession({ sessionId: `user-${req.user.id}-chat`, - model: "gpt-4.1", + model: "gpt-5.4", availableTools: ["custom:*"], gitHubToken: req.user.githubToken, }); @@ -355,7 +355,7 @@ const client = new CopilotClient({ }); const session = await client.createSession({ - model: "gpt-4.1", + model: "gpt-5.4", provider: { type: "openai", baseUrl: "https://api.openai.com/v1", @@ -407,7 +407,7 @@ app.post("/api/chat", async (req, res) => { } catch { session = await client.createSession({ sessionId, - model: "gpt-4.1", + model: "gpt-5.4", availableTools: ["custom:*"], gitHubToken: req.user.githubToken, }); @@ -436,7 +436,7 @@ const client = new CopilotClient({ async function processJob(job: Job) { const session = await client.createSession({ sessionId: `job-${job.id}`, - model: "gpt-4.1", + model: "gpt-5.4", }); const response = await session.sendAndWait({ diff --git a/docs/setup/bundled-cli.md b/docs/setup/bundled-cli.md index 26eb62c3f4..7c7d2fbbc4 100644 --- a/docs/setup/bundled-cli.md +++ b/docs/setup/bundled-cli.md @@ -47,7 +47,7 @@ import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient(); -const session = await client.createSession({ model: "gpt-4.1" }); +const session = await client.createSession({ model: "gpt-5.4" }); const response = await session.sendAndWait({ prompt: "Hello!" }); console.log(response?.data.content); @@ -66,7 +66,7 @@ from copilot.session import PermissionHandler client = CopilotClient() await client.start() -session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1") +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5.4") response = await session.send_and_wait("Hello!") print(response.data.content) @@ -101,7 +101,7 @@ func main() { } defer client.Stop() - session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-4.1"}) + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-5.4"}) response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) if d, ok := response.Data.(*copilot.AssistantMessageData); ok { fmt.Println(d.Content) @@ -117,7 +117,7 @@ if err := client.Start(ctx); err != nil { } defer client.Stop() -session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-4.1"}) +session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-5.4"}) response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) if d, ok := response.Data.(*copilot.AssistantMessageData); ok { fmt.Println(d.Content) @@ -132,7 +132,7 @@ if d, ok := response.Data.(*copilot.AssistantMessageData); ok { ```csharp await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync( - new SessionConfig { Model = "gpt-4.1" }); + new SessionConfig { Model = "gpt-5.4" }); var response = await session.SendAndWaitAsync( new MessageOptions { Prompt = "Hello!" }); @@ -158,7 +158,7 @@ var client = new CopilotClient(new CopilotClientOptions() client.start().get(); var session = client.createSession(new SessionConfig() - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); @@ -219,7 +219,7 @@ If you manage your own model provider keys, users don't need GitHub accounts at const client = new CopilotClient(); const session = await client.createSession({ - model: "gpt-4.1", + model: "gpt-5.4", provider: { type: "openai", baseUrl: "https://api.openai.com/v1", @@ -241,7 +241,7 @@ const client = new CopilotClient(); const sessionId = `project-${projectName}`; const session = await client.createSession({ sessionId, - model: "gpt-4.1", + model: "gpt-5.4", }); // User closes app... diff --git a/docs/setup/choosing-a-setup-path.md b/docs/setup/choosing-a-setup-path.md index 7fe28be8d4..17c971e657 100644 --- a/docs/setup/choosing-a-setup-path.md +++ b/docs/setup/choosing-a-setup-path.md @@ -88,7 +88,7 @@ Use this table to find the right guides based on what you need to do: | Getting started quickly | [Default Setup (Bundled CLI)](./bundled-cli.md) | | Use your own CLI binary or server | [Local CLI](./local-cli.md) | | Users sign in with GitHub | [GitHub OAuth](./github-oauth.md) | -| Use your own model keys (OpenAI, Azure, etc.) | [BYOK](../auth/byok.md) | +| Use your own model keys (OpenAI, Azure, and more) | [BYOK](../auth/byok.md) | | Azure BYOK with Managed Identity (no API keys) | [Azure Managed Identity](./azure-managed-identity.md) | | Run the SDK on a server | [Backend Services](./backend-services.md) | | Configure SDK options for concurrent users | [Multi-tenancy and server deployments](./multi-tenancy.md) | diff --git a/docs/setup/github-oauth.md b/docs/setup/github-oauth.md index 25b6aa80e3..5b44024b14 100644 --- a/docs/setup/github-oauth.md +++ b/docs/setup/github-oauth.md @@ -133,7 +133,7 @@ function createClientForUser(userToken: string): CopilotClient { const client = createClientForUser("gho_user_access_token"); const session = await client.createSession({ sessionId: `user-${userId}-session`, - model: "gpt-4.1", + model: "gpt-5.4", }); const response = await session.sendAndWait({ prompt: "Hello!" }); @@ -158,7 +158,7 @@ def create_client_for_user(user_token: str) -> CopilotClient: client = create_client_for_user("gho_user_access_token") await client.start() -session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", session_id=f"user-{user_id}-session") +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5.4", session_id=f"user-{user_id}-session") response = await session.send_and_wait("Hello!") ``` @@ -195,7 +195,7 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ SessionID: fmt.Sprintf("user-%s-session", userID), - Model: "gpt-4.1", + Model: "gpt-5.4", }) response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) _ = response @@ -218,7 +218,7 @@ defer client.Stop() session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ SessionID: fmt.Sprintf("user-%s-session", userID), - Model: "gpt-4.1", + Model: "gpt-5.4", }) response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) ``` @@ -245,7 +245,7 @@ await using var client = CreateClientForUser("gho_user_access_token"); await using var session = await client.CreateSessionAsync(new SessionConfig { SessionId = $"user-{userId}-session", - Model = "gpt-4.1", + Model = "gpt-5.4", }); var response = await session.SendAndWaitAsync( @@ -266,7 +266,7 @@ await using var client = CreateClientForUser("gho_user_access_token"); await using var session = await client.CreateSessionAsync(new SessionConfig { SessionId = $"user-{userId}-session", - Model = "gpt-4.1", + Model = "gpt-5.4", }); var response = await session.SendAndWaitAsync( @@ -297,7 +297,7 @@ var userId = "user1"; try (var client = createClientForUser("gho_user_access_token")) { var session = client.createSession(new SessionConfig() .setSessionId(String.format("user-%s-session", userId)) - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); diff --git a/docs/setup/local-cli.md b/docs/setup/local-cli.md index b8dd735b38..72394b3481 100644 --- a/docs/setup/local-cli.md +++ b/docs/setup/local-cli.md @@ -40,7 +40,7 @@ const client = new CopilotClient({ cliPath: "/usr/local/bin/copilot", }); -const session = await client.createSession({ model: "gpt-4.1" }); +const session = await client.createSession({ model: "gpt-5.4" }); const response = await session.sendAndWait({ prompt: "Hello!" }); console.log(response?.data.content); @@ -62,7 +62,7 @@ client = CopilotClient({ }) await client.start() -session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1") +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5.4") response = await session.send_and_wait("Hello!") if response: match response.data: @@ -102,7 +102,7 @@ func main() { } defer client.Stop() - session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-4.1"}) + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-5.4"}) response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) if response != nil { if d, ok := response.Data.(*copilot.AssistantMessageData); ok { @@ -122,7 +122,7 @@ if err := client.Start(ctx); err != nil { } defer client.Stop() -session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-4.1"}) +session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-5.4"}) response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) if response != nil { if d, ok := response.Data.(*copilot.AssistantMessageData); ok { @@ -143,7 +143,7 @@ var client = new CopilotClient(new CopilotClientOptions }); await using var session = await client.CreateSessionAsync( - new SessionConfig { Model = "gpt-4.1" }); + new SessionConfig { Model = "gpt-5.4" }); var response = await session.SendAndWaitAsync( new MessageOptions { Prompt = "Hello!" }); @@ -190,7 +190,7 @@ Sessions default to ephemeral. To create resumable sessions, provide your own se // Create a named session const session = await client.createSession({ sessionId: "my-project-analysis", - model: "gpt-4.1", + model: "gpt-5.4", }); // Later, resume it diff --git a/docs/setup/multi-tenancy.md b/docs/setup/multi-tenancy.md index def44746e6..2f82dde0bf 100644 --- a/docs/setup/multi-tenancy.md +++ b/docs/setup/multi-tenancy.md @@ -48,7 +48,7 @@ const client = new CopilotClient({ const session = await client.createSession({ sessionId: `user-${user.id}-${crypto.randomUUID()}`, - model: "gpt-4.1", + model: "gpt-5.4", availableTools: ["custom:lookupOrder", "custom:createTicket"], gitHubToken: user.githubToken, }); @@ -73,7 +73,7 @@ await client.start() session = await client.create_session( session_id=f"user-{user.id}-{request_id}", - model="gpt-4.1", + model="gpt-5.4", available_tools=["custom:lookupOrder", "custom:createTicket"], github_token=user.github_token, on_permission_request=PermissionHandler.approve_all, @@ -117,7 +117,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ SessionID: fmt.Sprintf("user-%s-%s", user.ID, requestID), - Model: "gpt-4.1", + Model: "gpt-5.4", AvailableTools: []string{"custom:lookupOrder", "custom:createTicket"}, GitHubToken: user.GitHubToken, }) @@ -137,7 +137,7 @@ client := copilot.NewClient(&copilot.ClientOptions{ session, err := client.CreateSession(ctx, &copilot.SessionConfig{ SessionID: fmt.Sprintf("user-%s-%s", user.ID, requestID), - Model: "gpt-4.1", + Model: "gpt-5.4", AvailableTools: []string{"custom:lookupOrder", "custom:createTicket"}, GitHubToken: user.GitHubToken, }) @@ -168,7 +168,7 @@ var client = new CopilotClient(new CopilotClientOptions await using var session = await client.CreateSessionAsync(new SessionConfig { SessionId = $"user-{user.Id}-{requestId}", - Model = "gpt-4.1", + Model = "gpt-5.4", AvailableTools = ["custom:lookupOrder", "custom:createTicket"], GitHubToken = user.GitHubToken, }); @@ -187,7 +187,7 @@ var client = new CopilotClient(new CopilotClientOptions await using var session = await client.CreateSessionAsync(new SessionConfig { SessionId = $"user-{user.Id}-{requestId}", - Model = "gpt-4.1", + Model = "gpt-5.4", AvailableTools = ["custom:lookupOrder", "custom:createTicket"], GitHubToken = user.GitHubToken, }); @@ -223,7 +223,7 @@ public class MultiTenancyExample { var session = client.createSession(new SessionConfig() .setSessionId("user-" + user.id() + "-" + requestId) - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setAvailableTools(List.of("custom:lookupOrder", "custom:createTicket")) .setGitHubToken(user.gitHubToken()) ).get(); @@ -242,7 +242,7 @@ var client = new CopilotClient(new CopilotClientOptions() var session = client.createSession(new SessionConfig() .setSessionId("user-" + user.id() + "-" + requestId) - .setModel("gpt-4.1") + .setModel("gpt-5.4") .setAvailableTools(List.of("custom:lookupOrder", "custom:createTicket")) .setGitHubToken(user.gitHubToken()) ).get(); @@ -276,7 +276,7 @@ let client = Client::start( let session = client.create_session( SessionConfig::default() .with_session_id(format!("user-{}-{request_id}", user.id)) - .with_model("gpt-4.1") + .with_model("gpt-5.4") .with_available_tools(["custom:lookupOrder", "custom:createTicket"]) .with_github_token(user.github_token), ).await?; @@ -366,7 +366,7 @@ Set `gitHubToken` on each session to scope GitHub auth to the requesting user. T ```typescript const session = await client.createSession({ sessionId: `user-${user.id}-support`, - model: "gpt-4.1", + model: "gpt-5.4", availableTools: ["custom:*"], gitHubToken: user.githubToken, }); diff --git a/docs/setup/scaling.md b/docs/setup/scaling.md index d960eb94ea..c4a7a0953f 100644 --- a/docs/setup/scaling.md +++ b/docs/setup/scaling.md @@ -324,7 +324,7 @@ app.post("/chat", async (req, res) => { const session = await client.createSession({ sessionId: `user-${req.user.id}-chat`, - model: "gpt-4.1", + model: "gpt-5.4", }); const response = await session.sendAndWait({ prompt: req.body.message }); @@ -404,7 +404,7 @@ class SessionManager { // Create or resume const session = await client.createSession({ sessionId, - model: "gpt-4.1", + model: "gpt-5.4", }); this.activeSessions.set(sessionId, session); @@ -450,7 +450,7 @@ For stateless API endpoints where each request is independent: ```typescript app.post("/api/analyze", async (req, res) => { const session = await client.createSession({ - model: "gpt-4.1", + model: "gpt-5.4", }); try { @@ -475,7 +475,7 @@ app.post("/api/chat/start", async (req, res) => { const session = await client.createSession({ sessionId, - model: "gpt-4.1", + model: "gpt-5.4", infiniteSessions: { enabled: true, backgroundCompactionThreshold: 0.80, diff --git a/docs/troubleshooting/mcp-debugging.md b/docs/troubleshooting/mcp-debugging.md index 3447ed9210..f93f7acae2 100644 --- a/docs/troubleshooting/mcp-debugging.md +++ b/docs/troubleshooting/mcp-debugging.md @@ -446,10 +446,10 @@ When opening an issue or asking for help, collect: * [ ] SDK language and version * [ ] CLI version (`copilot --version`) -* [ ] MCP server type (Node.js, Python, .NET, Go, Rust, etc.) +* [ ] MCP server type (Node.js, Python, .NET, Go, Rust, and more) * [ ] Full MCP server configuration (redact secrets) * [ ] Result of manual `initialize` test -* [ ] Result of manual `tools/list` test +* [ ] Result of manual `tools/list` test * [ ] Debug logs from SDK * [ ] Any error messages diff --git a/dotnet/src/Canvas.cs b/dotnet/src/Canvas.cs index b4e63f1b31..6bf8be984e 100644 --- a/dotnet/src/Canvas.cs +++ b/dotnet/src/Canvas.cs @@ -57,6 +57,32 @@ public sealed class ExtensionInfo public string Name { get; set; } = string.Empty; } +/// +/// Stable identity for a host/SDK connection that supplies built-in canvases. +/// +/// +/// When set on session create or resume, the runtime uses +/// verbatim as the agent-facing canvas extension id, so canvases declared on a +/// control connection survive stdio reconnect and CLI process restart instead +/// of being re-keyed to a per-connection id. The id is opaque to the runtime; a +/// per-window-stable value such as app:builtin:<windowId> is +/// recommended. An id beginning with connection: is reserved and ignored +/// by the runtime. +/// +[Experimental(Diagnostics.Experimental)] +public sealed class CanvasProviderIdentity +{ + /// + /// Opaque, stable provider id used verbatim as the canvas extension id. + /// + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Optional display name surfaced as the canvas extension name. + [JsonPropertyName("name")] + public string? Name { get; set; } +} + /// Structured exception returned from canvas handlers. /// /// Throw this from implementations to surface a diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 94b0921995..a512978225 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -15,6 +15,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text.RegularExpressions; @@ -237,6 +238,17 @@ private static void ValidateEnvironmentOptions(CopilotClientOptions options, Run nameof(options)); } + if (options.WorkingDirectory is not null) + { + throw new ArgumentException( + $"{nameof(CopilotClientOptions)}.{nameof(CopilotClientOptions.WorkingDirectory)} is not supported with " + + $"{nameof(RuntimeConnection)}.{nameof(RuntimeConnection.ForInProcess)}(): the in-process transport hosts " + + "the native runtime in the shared host process and spawns the worker without a working-directory " + + "parameter, so a per-client working directory cannot be honored in-process. Use a child-process " + + "transport, or set the process working directory before creating the client.", + nameof(options)); + } + return; } @@ -338,16 +350,49 @@ async Task StartCoreAsync(CancellationToken ct) { if (_connection is InProcessRuntimeConnection) { - // In-process FFI hosting: load the Rust cdylib and let it spawn - // the CLI worker, instead of the SDK launching a CLI child process. - // The worker reads its configuration (telemetry export, etc.) from - // the environment passed here, so apply the same telemetry-derived - // vars the child-process path sets on its startInfo.Environment. - var ffiEnvironment = _options.Environment?.ToDictionary(kvp => kvp.Key, kvp => (string?)kvp.Value) - ?? new Dictionary(); - ApplyTelemetryEnvironment(ffiEnvironment, _options.Telemetry); - var resolvedFfiEnvironment = ffiEnvironment.ToDictionary(kvp => kvp.Key, kvp => kvp.Value!); - var ffiHost = FfiRuntimeHost.Create(ResolveCliPathForFfi(), GetNapiPrebuildsFolderOrThrow(), resolvedFfiEnvironment, _logger); + var ffiEnvironment = new Dictionary(); + if (!string.IsNullOrEmpty(_options.GitHubToken)) + { + ffiEnvironment["COPILOT_SDK_AUTH_TOKEN"] = _options.GitHubToken!; + } + if (!string.IsNullOrEmpty(_options.BaseDirectory)) + { + ffiEnvironment["COPILOT_HOME"] = _options.BaseDirectory!; + } + if (_options.Mode == CopilotClientMode.Empty) + { + ffiEnvironment["COPILOT_DISABLE_KEYTAR"] = "1"; + } + + var ffiArgs = new List(); + if (_options.LogLevel is { } logLevel && !string.IsNullOrEmpty(logLevel.Value)) + { + ffiArgs.AddRange(["--log-level", logLevel.Value]); + } + if (!string.IsNullOrEmpty(_options.GitHubToken)) + { + ffiArgs.AddRange(["--auth-token-env", "COPILOT_SDK_AUTH_TOKEN"]); + } + var useLoggedInUser = _options.UseLoggedInUser ?? string.IsNullOrEmpty(_options.GitHubToken); + if (!useLoggedInUser) + { + ffiArgs.Add("--no-auto-login"); + } + if (_options.SessionIdleTimeoutSeconds is > 0) + { + ffiArgs.AddRange(["--session-idle-timeout", _options.SessionIdleTimeoutSeconds.Value.ToString(CultureInfo.InvariantCulture)]); + } + if (_options.EnableRemoteSessions) + { + ffiArgs.Add("--remote"); + } + + var ffiHost = FfiRuntimeHost.Create( + ResolveCliPathForFfi(), + GetNapiPrebuildsFolderOrThrow(), + ffiEnvironment, + ffiArgs, + _logger); _ffiHost = ffiHost; await ffiHost.StartAsync(ct); connection = await ConnectToServerAsync(null, null, null, null, ct, ffiHost); @@ -1137,16 +1182,19 @@ public async Task CreateSessionAsync(SessionConfig config, Cance InstructionDirectories: config.InstructionDirectories, PluginDirectories: config.PluginDirectories, LargeOutput: config.LargeOutput, + ToolSearch: config.ToolSearch, Memory: config.Memory, Canvases: config.Canvases, RequestCanvasRenderer: config.RequestCanvasRenderer, RequestExtensions: config.RequestExtensions, ExtensionSdkPath: config.ExtensionSdkPath, ExtensionInfo: config.ExtensionInfo, + CanvasProvider: config.CanvasProvider, Providers: config.Providers, Models: config.Models, ToolFilterPrecedence: toolFilter.ToolFilterPrecedence, ExpAssignments: config.ExpAssignments, + EnableManagedSettings: config.EnableManagedSettings, EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null); var rpcTimestamp = Stopwatch.GetTimestamp(); @@ -1346,17 +1394,20 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes InstructionDirectories: config.InstructionDirectories, PluginDirectories: config.PluginDirectories, LargeOutput: config.LargeOutput, + ToolSearch: config.ToolSearch, Memory: config.Memory, Canvases: config.Canvases, RequestCanvasRenderer: config.RequestCanvasRenderer, RequestExtensions: config.RequestExtensions, ExtensionSdkPath: config.ExtensionSdkPath, ExtensionInfo: config.ExtensionInfo, + CanvasProvider: config.CanvasProvider, OpenCanvases: config.OpenCanvases, Providers: config.Providers, Models: config.Models, ToolFilterPrecedence: toolFilter.ToolFilterPrecedence, ExpAssignments: config.ExpAssignments, + EnableManagedSettings: config.EnableManagedSettings, EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null); var rpcTimestamp = Stopwatch.GetTimestamp(); @@ -2197,7 +2248,12 @@ private static void ApplyTelemetryEnvironment(IDictionary envir { string os; if (OperatingSystem.IsWindows()) os = "win"; - else if (OperatingSystem.IsLinux()) os = "linux"; + else if (OperatingSystem.IsLinux()) + { + os = RuntimeInformation.RuntimeIdentifier.StartsWith("linux-musl-", StringComparison.Ordinal) + ? "linux-musl" + : "linux"; + } else if (OperatingSystem.IsMacOS()) os = "osx"; else return null; @@ -2244,7 +2300,12 @@ private string ResolveCliPathForFfi() { string platform; if (OperatingSystem.IsWindows()) platform = "win32"; - else if (OperatingSystem.IsLinux()) platform = "linux"; + else if (OperatingSystem.IsLinux()) + { + platform = RuntimeInformation.RuntimeIdentifier.StartsWith("linux-musl-", StringComparison.Ordinal) + ? "linuxmusl" + : "linux"; + } else if (OperatingSystem.IsMacOS()) platform = "darwin"; else return null; @@ -2685,6 +2746,7 @@ internal record CreateSessionRequest( IList? InstructionDirectories = null, IList? PluginDirectories = null, LargeToolOutputConfig? LargeOutput = null, + ToolSearchConfig? ToolSearch = null, MemoryConfiguration? Memory = null, #pragma warning disable GHCP001 IList? Canvases = null, @@ -2692,10 +2754,12 @@ internal record CreateSessionRequest( bool? RequestExtensions = null, string? ExtensionSdkPath = null, ExtensionInfo? ExtensionInfo = null, + CanvasProviderIdentity? CanvasProvider = null, IList? Providers = null, IList? Models = null, OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null, [property: JsonPropertyName("expAssignments")] JsonElement? ExpAssignments = null, + [property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null, bool? EnableGitHubTelemetryForwarding = null); #pragma warning restore GHCP001 @@ -2705,17 +2769,20 @@ internal record ToolDefinition( JsonElement Parameters, /* JSON schema */ bool? OverridesBuiltInTool = null, bool? SkipPermission = null, - CopilotToolDefer? Defer = null) + CopilotToolDefer? Defer = null, + IDictionary? Metadata = null) { public static ToolDefinition FromAIFunction(AIFunctionDeclaration function) { var overrides = function.AdditionalProperties.TryGetValue(CopilotTool.OverridesBuiltInToolKey, out var val) && val is true; var skipPerm = function.AdditionalProperties.TryGetValue(CopilotTool.SkipPermissionKey, out var skipVal) && skipVal is true; var defer = function.AdditionalProperties.TryGetValue(CopilotTool.DeferKey, out var deferVal) && deferVal is CopilotToolDefer d ? d : (CopilotToolDefer?)null; + var metadata = function.AdditionalProperties.TryGetValue(CopilotTool.MetadataKey, out var metaVal) && metaVal is IDictionary m ? m : null; return new ToolDefinition(function.Name, function.Description, function.JsonSchema, overrides ? true : null, skipPerm ? true : null, - defer); + defer, + metadata); } } @@ -2784,6 +2851,7 @@ internal record ResumeSessionRequest( IList? InstructionDirectories = null, IList? PluginDirectories = null, LargeToolOutputConfig? LargeOutput = null, + ToolSearchConfig? ToolSearch = null, MemoryConfiguration? Memory = null, #pragma warning disable GHCP001 IList? Canvases = null, @@ -2791,11 +2859,13 @@ internal record ResumeSessionRequest( bool? RequestExtensions = null, string? ExtensionSdkPath = null, ExtensionInfo? ExtensionInfo = null, + CanvasProviderIdentity? CanvasProvider = null, IList? OpenCanvases = null, IList? Providers = null, IList? Models = null, OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null, [property: JsonPropertyName("expAssignments")] JsonElement? ExpAssignments = null, + [property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null, bool? EnableGitHubTelemetryForwarding = null); #pragma warning restore GHCP001 diff --git a/dotnet/src/CopilotRequestHandler.cs b/dotnet/src/CopilotRequestHandler.cs index f26fc70e0a..514d77da6f 100644 --- a/dotnet/src/CopilotRequestHandler.cs +++ b/dotnet/src/CopilotRequestHandler.cs @@ -49,6 +49,9 @@ public CopilotRequestContext(CopilotRequestContext original) : this(original.RequestId, original.Url, original.Headers) { SessionId = original.SessionId; + AgentId = original.AgentId; + ParentAgentId = original.ParentAgentId; + InteractionType = original.InteractionType; Transport = original.Transport; CancellationToken = original.CancellationToken; WebSocketResponse = original.WebSocketResponse; @@ -67,6 +70,15 @@ internal CopilotRequestContext(string requestId, string url, IReadOnlyDictionary /// Runtime session id that triggered the request, if any. public string? SessionId { get; init; } + /// Stable per-agent-instance id for the agent trajectory that issued this request. + public string? AgentId { get; init; } + + /// Id of the parent agent when this request was issued by a subagent. + public string? ParentAgentId { get; init; } + + /// Runtime classification for the interaction that produced this request. + public string? InteractionType { get; init; } + /// Transport the runtime would otherwise use. public CopilotRequestTransport Transport { get; init; } @@ -526,6 +538,12 @@ private static async Task BuildHttpRequestAsync(LlmInference if (!message.Headers.TryAddWithoutValidation(name, values)) { +#if NETSTANDARD2_0 + if (!hasBody) + { + continue; + } +#endif message.Content ??= new ByteArrayContent([]); message.Content.Headers.TryAddWithoutValidation(name, values); } @@ -870,6 +888,9 @@ public Task HttpRequestStartAsync(LlmInferen exchange.Context = new CopilotRequestContext(request.RequestId, request.Url, ToReadOnlyHeaders(request.Headers)) { SessionId = request.SessionId, + AgentId = request.AgentId, + ParentAgentId = request.ParentAgentId, + InteractionType = request.InteractionType, Transport = transport, CancellationToken = exchange.Abort.Token, }; diff --git a/dotnet/src/CopilotTool.cs b/dotnet/src/CopilotTool.cs index d6dc0351e4..e22296bccd 100644 --- a/dotnet/src/CopilotTool.cs +++ b/dotnet/src/CopilotTool.cs @@ -3,6 +3,7 @@ *--------------------------------------------------------------------------------------------*/ using Microsoft.Extensions.AI; +using System.Text.Json.Nodes; namespace GitHub.Copilot; @@ -20,6 +21,9 @@ public static class CopilotTool /// The key used in to carry the tool's deferral mode. internal const string DeferKey = "defer"; + /// The key used in to carry the tool's opaque host-defined metadata. + internal const string MetadataKey = "metadata"; + /// /// Defines a tool for use in a . /// @@ -87,7 +91,7 @@ static void ApplyToolInvocationBinding(AIFunctionFactoryOptions factoryOptions) static void ApplyToolOptions(AIFunctionFactoryOptions factoryOptions, CopilotToolOptions? toolOptions) { - if (toolOptions is not null && (toolOptions.OverridesBuiltInTool || toolOptions.SkipPermission || toolOptions.Defer is not null)) + if (toolOptions is not null && (toolOptions.OverridesBuiltInTool || toolOptions.SkipPermission || toolOptions.Defer is not null || toolOptions.Metadata is not null)) { Dictionary additionalProperties = new(StringComparer.Ordinal); if (factoryOptions.AdditionalProperties is not null) @@ -113,6 +117,11 @@ static void ApplyToolOptions(AIFunctionFactoryOptions factoryOptions, CopilotToo additionalProperties[DeferKey] = defer; } + if (toolOptions.Metadata is { } metadata) + { + additionalProperties[MetadataKey] = metadata; + } + factoryOptions.AdditionalProperties = additionalProperties; } } @@ -151,6 +160,11 @@ public sealed class CopilotToolOptions /// SDK forwards it to the CLI as the tool's defer mode. Defaults to "auto". /// public CopilotToolDefer? Defer { get; set; } + + /// + /// Gets or sets opaque, host-defined metadata associated with the tool definition. + /// + public IDictionary? Metadata { get; set; } } /// diff --git a/dotnet/src/FfiRuntimeHost.cs b/dotnet/src/FfiRuntimeHost.cs index 210819de67..a838b9fd17 100644 --- a/dotnet/src/FfiRuntimeHost.cs +++ b/dotnet/src/FfiRuntimeHost.cs @@ -44,6 +44,7 @@ internal sealed partial class FfiRuntimeHost : IDisposable private readonly string _cliEntrypoint; private readonly string _libraryPath; private readonly IReadOnlyDictionary? _environment; + private readonly IReadOnlyList _args; private readonly CallbackReceiveStream _receiveStream = new(); private CallbackSendStream? _sendStream; @@ -52,11 +53,12 @@ internal sealed partial class FfiRuntimeHost : IDisposable private uint _connectionId; private bool _disposed; - private FfiRuntimeHost(string libraryPath, string cliEntrypoint, IReadOnlyDictionary? environment, ILogger logger) + private FfiRuntimeHost(string libraryPath, string cliEntrypoint, IReadOnlyDictionary? environment, IReadOnlyList args, ILogger logger) { _libraryPath = libraryPath; _cliEntrypoint = cliEntrypoint; _environment = environment; + _args = args; _logger = logger; } @@ -79,7 +81,7 @@ private FfiRuntimeHost(string libraryPath, string cliEntrypoint, IReadOnlyDictio /// is the napi-rs /// <node-platform>-<arch> folder name (e.g. win32-x64). /// - public static FfiRuntimeHost Create(string cliEntrypoint, string prebuildsFolder, IReadOnlyDictionary? environment, ILogger logger) + public static FfiRuntimeHost Create(string cliEntrypoint, string prebuildsFolder, IReadOnlyDictionary? environment, IReadOnlyList args, ILogger logger) { var fullEntrypoint = Path.GetFullPath(cliEntrypoint); var distDir = Path.GetDirectoryName(fullEntrypoint) @@ -96,7 +98,7 @@ public static FfiRuntimeHost Create(string cliEntrypoint, string prebuildsFolder $"FFI runtime library not found. Looked for '{flatLibraryPath}' and '{prebuildsLibraryPath}'."); PrepareNativeLibrary(libraryPath); - return new FfiRuntimeHost(libraryPath, fullEntrypoint, environment, logger); + return new FfiRuntimeHost(libraryPath, fullEntrypoint, environment, args, logger); } /// @@ -122,7 +124,7 @@ public async Task StartAsync(CancellationToken cancellationToken) // perform the blocking FFI handshake on a background thread. await Task.Run(() => { - var argvJson = BuildArgvJson(_cliEntrypoint); + var argvJson = BuildArgvJson(_cliEntrypoint, _args); var envJson = BuildEnvJson(_environment); _serverId = NativeHostStart(argvJson, envJson); @@ -152,7 +154,7 @@ await Task.Run(() => } } - private static byte[] BuildArgvJson(string cliEntrypoint) + private static byte[] BuildArgvJson(string cliEntrypoint, IReadOnlyList args) { // A .js entrypoint (dev / dist-cli) is launched via node; the packaged // single-file CLI binary embeds its own Node and is invoked directly. @@ -167,6 +169,13 @@ private static byte[] BuildArgvJson(string cliEntrypoint) } writer.WriteStringValue(cliEntrypoint); writer.WriteStringValue("--embedded-host"); + // Pin the worker to the bundled pkg matching the loaded cdylib, instead of + // drifting to a newer version under the user's ~/.copilot/pkg (ABI skew). + writer.WriteStringValue("--no-auto-update"); + foreach (var arg in args) + { + writer.WriteStringValue(arg); + } writer.WriteEndArray(); } return stream.ToArray(); diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index c90fb90e16..bf73bdfaf7 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -74,6 +74,27 @@ internal sealed class ConnectRequest public string? Token { get; set; } } +/// Active server-driven promotion for a model, including its discount and expiry. +[Experimental(Diagnostics.Experimental)] +public sealed class ModelBillingPromo +{ + /// Percentage discount (0-100) applied while the promotion is active. May be fractional. + [JsonPropertyName("discountPercent")] + public double? DiscountPercent { get; set; } + + /// UTC ISO 8601 timestamp marking when the promotion ends. Always present: the API only surfaces a promo whose expiry parses and is in the future. Consumers should treat a past value as expired. + [JsonPropertyName("endsAt")] + public string EndsAt { get; set; } = string.Empty; + + /// Stable identifier for the promotion campaign. + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it. + [JsonPropertyName("message")] + public string? Message { get; set; } +} + /// Long context tier pricing (available for models with extended context windows). [Experimental(Diagnostics.Experimental)] public sealed class ModelBillingTokenPricesLongContext @@ -176,6 +197,10 @@ public sealed class ModelBilling [JsonPropertyName("multiplier")] public double? Multiplier { get; set; } + /// Active server-driven promotion for this model, if any. Present when the model is being promoted with a time-boxed discount. + [JsonPropertyName("promo")] + public ModelBillingPromo? Promo { get; set; } + /// Token-level pricing information for this model. [JsonPropertyName("tokenPrices")] public ModelBillingTokenPrices? TokenPrices { get; set; } @@ -1339,13 +1364,17 @@ public sealed class MarketplaceAddResult public string Name { get; set; } = string.Empty; } -/// Marketplace source to register. +/// Marketplace source and optional working directory for relative-path resolution. [Experimental(Diagnostics.Experimental)] internal sealed class PluginsMarketplacesAddRequest { /// Marketplace source. Accepts the same forms as the CLI: "owner/repo" or "owner/repo#ref" (GitHub), an http/https/ssh URL (optionally with #ref), a git scp-style URL (user@host:path), or a local path. The marketplace's own name (from its manifest) is used as the registration key. [JsonPropertyName("source")] public string Source { get; set; } = string.Empty; + + /// Working directory used to resolve relative local paths in `source`. Defaults to the server's current working directory. + [JsonPropertyName("workingDirectory")] + public string? WorkingDirectory { get; set; } } /// Outcome of the remove attempt, including dependent-plugin info when applicable. @@ -1784,6 +1813,90 @@ internal sealed class InstructionsGetDiscoveryPathsRequest public IList? ProjectPaths { get; set; } } +/// A literal choice the command input accepts, with a human-facing description. +[Experimental(Diagnostics.Experimental)] +public sealed class SlashCommandInputChoice +{ + /// Human-readable description shown alongside the choice. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// The literal choice value (e.g. 'on', 'off', 'show'). + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} + +/// Optional unstructured input hint. +[Experimental(Diagnostics.Experimental)] +public sealed class SlashCommandInput +{ + /// Optional literal choices the input accepts, each with a human-facing description; clients may render these as selectable options. + [JsonPropertyName("choices")] + public IList? Choices { get; set; } + + /// Optional completion hint for the input (e.g. 'directory' for filesystem path completion). + [JsonPropertyName("completion")] + public SlashCommandInputCompletion? Completion { get; set; } + + /// Hint to display when command input has not been provided. + [JsonPropertyName("hint")] + public string Hint { get; set; } = string.Empty; + + /// When true, clients should pass the full text after the command name as a single argument rather than splitting on whitespace. + [JsonPropertyName("preserveMultilineInput")] + public bool? PreserveMultilineInput { get; set; } + + /// When true, the command requires non-empty input; clients should render the input hint as required. + [JsonPropertyName("required")] + public bool? Required { get; set; } +} + +/// Slash-command metadata with name, aliases, description, kind, input hint, execution allowance, and schedulability. +[Experimental(Diagnostics.Experimental)] +public sealed class SlashCommandInfo +{ + /// Canonical aliases without leading slashes. + [JsonPropertyName("aliases")] + public IList? Aliases { get; set; } + + /// Whether the command may run while an agent turn is active. + [JsonPropertyName("allowDuringAgentExecution")] + public bool AllowDuringAgentExecution { get; set; } + + /// Human-readable command description. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// Whether the command is experimental. + [JsonPropertyName("experimental")] + public bool? Experimental { get; set; } + + /// Optional unstructured input hint. + [JsonPropertyName("input")] + public SlashCommandInput? Input { get; set; } + + /// Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command. + [JsonPropertyName("kind")] + public SlashCommandKind Kind { get; set; } + + /// Canonical command name without a leading slash. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Whether the command may be the target of `/every` / `/after` schedules. Resolution happens at every tick, so only set this when the command is safe to re-invoke and produces an agent prompt. + [JsonPropertyName("schedulable")] + public bool? Schedulable { get; set; } +} + +/// Slash commands available in the session, after applying any include/exclude filters. +[Experimental(Diagnostics.Experimental)] +public sealed class CommandList +{ + /// Commands available in this session. + [JsonPropertyName("commands")] + public IList Commands { get => field ??= []; set; } +} + /// A single user setting's effective value alongside its default, so consumers can render settings left at their default. [Experimental(Diagnostics.Experimental)] public sealed class UserSettingMetadata @@ -2818,6 +2931,12 @@ public partial class RemoteControlStatusActive : RemoteControlStatus [JsonPropertyName("attachedSessionId")] public required string AttachedSessionId { get; set; } + /// True while a read-only/session-sync export is deferred, awaiting the first `user.message` before its MC session exists. Marked internal: this field is excluded from the public SDK surface and is populated only on the CLI in-process path. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] + [JsonPropertyName("awaitingFirstMessage")] + internal bool? AwaitingFirstMessage { get; set; } + /// MC frontend URL for this session, when known. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("frontendUrl")] @@ -3347,6 +3466,88 @@ internal sealed class SendRequest public bool? Wait { get; set; } } +/// Result of sending zero or more user messages. +[Experimental(Diagnostics.Experimental)] +public sealed class SendMessagesResult +{ + /// Unique identifiers assigned to the messages, one per provided message in order. Empty when no messages were provided. + [JsonPropertyName("messageIds")] + public IList MessageIds { get => field ??= []; set; } +} + +/// A single user message to append to the session as part of a `session.sendMessages` turn. +[Experimental(Diagnostics.Experimental)] +public sealed class SendMessageItem +{ + /// Optional attachments (files, directories, selections, blobs, GitHub references) to include with this message. + [JsonPropertyName("attachments")] + public IList? Attachments { get; set; } + + /// If false, this message will not trigger a Premium Request Unit charge. User messages default to billable. + [JsonInclude] + [JsonPropertyName("billable")] + internal bool? Billable { get; set; } + + /// If provided, this is shown in the timeline instead of `prompt`. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } + + /// The user message text. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; + + /// If set, the request will fail if the named tool is not available when this message is among the user messages at the start of the current exchange. + [JsonPropertyName("requiredTool")] + public string? RequiredTool { get; set; } + + /// Optional provenance tag copied to the resulting user.message event. Must match one of three forms: the literal `system`, `command-<command-id>` for messages originating from a command (e.g. slash command, Mission Control command), or `schedule-<numeric-id>` for messages originating from a scheduled job. + [RegularExpression("^(system|command-.*|schedule-\\d+)$")] + [JsonInclude] + [JsonPropertyName("source")] + internal string? Source { get; set; } +} + +/// Parameters for sending zero or more user messages to the session in a single turn. Remote-backed (Mission Control) sessions do not support this method and will return an error. +[Experimental(Diagnostics.Experimental)] +internal sealed class SendMessagesRequest +{ + /// The UI mode the agent was in when these messages were sent. Defaults to the session's current mode. + [JsonPropertyName("agentMode")] + public SendAgentMode? AgentMode { get; set; } + + /// The user messages to append to the conversation, in order. May be empty, in which case a single turn runs over the existing history with no new user message. + [JsonPropertyName("messages")] + public IList Messages { get => field ??= []; set; } + + /// How to deliver the messages. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. + [JsonPropertyName("mode")] + public SendMode? Mode { get; set; } + + /// If true, adds the messages to the front of the queue instead of the end. + [JsonPropertyName("prepend")] + public bool? Prepend { get; set; } + + /// Custom HTTP headers to include in outbound model requests for this turn. Merged with session-level provider headers; per-turn headers augment and overwrite session-level headers with the same key. + [JsonPropertyName("requestHeaders")] + public IDictionary? RequestHeaders { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// W3C Trace Context traceparent header for distributed tracing of this agent turn. + [JsonPropertyName("traceparent")] + public string? Traceparent { get; set; } + + /// W3C Trace Context tracestate header for distributed tracing. + [JsonPropertyName("tracestate")] + public string? Tracestate { get; set; } + + /// If true, await completion of the agentic loop for this turn before returning. Defaults to false (fire-and-forget). When true, the result still contains the same `messageIds`; the caller can rely on the agent having processed the messages before the call resolves. + [JsonPropertyName("wait")] + public bool? Wait { get; set; } +} + /// Result of aborting the current turn. [Experimental(Diagnostics.Experimental)] public sealed class AbortResult @@ -3728,6 +3929,10 @@ public sealed class DiscoveredCanvas [JsonPropertyName("extensionName")] public string? ExtensionName { get; set; } + /// Host-local PNG path for the canvas icon, when supplied. + [JsonPropertyName("icon")] + public string? Icon { get; set; } + /// JSON Schema for canvas open input. [JsonPropertyName("inputSchema")] public JsonElement? InputSchema { get; set; } @@ -3767,6 +3972,10 @@ public sealed class OpenCanvasInstance [JsonPropertyName("extensionName")] public string? ExtensionName { get; set; } + /// Host-local PNG path for the canvas icon, when supplied. + [JsonPropertyName("icon")] + public string? Icon { get; set; } + /// Input supplied when the instance was opened. [JsonPropertyName("input")] public JsonElement? Input { get; set; } @@ -4004,6 +4213,10 @@ internal sealed class ModelSwitchToRequest /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// Output verbosity level to request for supported models. + [JsonPropertyName("verbosity")] + public Verbosity? Verbosity { get; set; } } /// 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. @@ -4028,6 +4241,19 @@ internal sealed class ModelSetReasoningEffortRequest public string SessionId { get; set; } = string.Empty; } +/// Cost-category metadata for a CAPI model. +[Experimental(Diagnostics.Experimental)] +public sealed class SessionModelPriceCategory +{ + /// Gets or sets the id value. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Gets or sets the priceCategory value. + [JsonPropertyName("priceCategory")] + public ModelPickerPriceCategory PriceCategory { get; set; } +} + /// The list of models available to this session. [Experimental(Diagnostics.Experimental)] public sealed class SessionModelList @@ -4036,6 +4262,10 @@ public sealed class SessionModelList [JsonPropertyName("list")] public IList List { get => field ??= []; set; } + /// Cost categories for the full CAPI catalog, including picker-disabled models that Auto may select. Metadata only; entries absent from `list` are not manually selectable. + [JsonPropertyName("modelPriceCategories")] + public IList? ModelPriceCategories { get; set; } + /// Per-quota snapshots returned alongside the model list, keyed by quota type. [JsonPropertyName("quotaSnapshots")] public IDictionary? QuotaSnapshots { get; set; } @@ -5512,7 +5742,20 @@ internal sealed class SessionMcpListRequest public string SessionId { get; set; } = string.Empty; } -/// MCP tool metadata with tool name and optional description. +/// Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block. +[Experimental(Diagnostics.Experimental)] +public sealed class McpToolUi +{ + /// URI of the tool's MCP App resource, typically a `ui://` resource identifier. Use `session.mcp.resources.read` to fetch its HTML and resource metadata. + [JsonPropertyName("resourceUri")] + public string? ResourceUri { get; set; } + + /// Tool visibility advertised by the server. When absent, MCP Apps defaults apply. + [JsonPropertyName("visibility")] + public IList? Visibility { get; set; } +} + +/// MCP tool metadata with tool name, optional description, and normalized MCP Apps discovery metadata. [Experimental(Diagnostics.Experimental)] public sealed class McpTools { @@ -5523,6 +5766,10 @@ public sealed class McpTools /// Tool name. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; + + /// Normalized MCP Apps discovery metadata. An empty object indicates that a valid `_meta.ui` block was present without recognized fields. + [JsonPropertyName("ui")] + public McpToolUi? Ui { get; set; } } /// Tools exposed by the connected MCP server. Throws when the server is not connected. @@ -5791,14 +6038,13 @@ internal sealed class McpConfigureGitHubRequest public string SessionId { get; set; } = string.Empty; } -/// Server name and opaque configuration for an individual MCP server start. +/// Server name and configuration for an individual MCP server start. [Experimental(Diagnostics.Experimental)] internal sealed class McpStartServerRequest { - /// Opaque server configuration (MCPServerConfig). Marked internal: an in-process runtime shape supplied only by in-process CLI callers. - [JsonInclude] + /// MCP server configuration (stdio process or remote HTTP/SSE). [JsonPropertyName("config")] - internal JsonElement Config { get; set; } + public JsonElement Config { get; set; } /// Name of the MCP server to start. [JsonPropertyName("serverName")] @@ -5809,14 +6055,13 @@ internal sealed class McpStartServerRequest public string SessionId { get; set; } = string.Empty; } -/// Server name and opaque configuration for an individual MCP server restart. +/// Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. [Experimental(Diagnostics.Experimental)] internal sealed class McpRestartServerRequest { - /// Opaque server configuration (MCPServerConfig). Marked internal: an in-process runtime shape supplied only by in-process CLI callers. - [JsonInclude] + /// Replacement MCP server configuration (stdio process or remote HTTP/SSE). Omit to restart the server with its already-registered configuration (config-free restart-by-name). [JsonPropertyName("config")] - internal JsonElement Config { get; set; } + public JsonElement? Config { get; set; } /// Name of the MCP server to restart. [JsonPropertyName("serverName")] @@ -6102,7 +6347,7 @@ public sealed class McpAppsResourceContent { /// Resource-level metadata (CSP, permissions, etc.). [JsonPropertyName("_meta")] - public IDictionary? _meta { get; set; } + public IDictionary? Meta { get; set; } /// Base64-encoded binary content. [JsonPropertyName("blob")] @@ -6322,50 +6567,302 @@ public sealed class McpAppsDiagnoseCapability [JsonPropertyName("featureFlagEnabled")] public bool FeatureFlagEnabled { get; set; } - /// Whether the session has the `mcp-apps` capability. - [JsonPropertyName("sessionHasMcpApps")] - public bool SessionHasMcpApps { get; set; } -} + /// Whether the session has the `mcp-apps` capability. + [JsonPropertyName("sessionHasMcpApps")] + public bool SessionHasMcpApps { get; set; } +} + +/// What the server returned for this session. +[Experimental(Diagnostics.Experimental)] +public sealed class McpAppsDiagnoseServer +{ + /// Whether the named server is currently connected. + [JsonPropertyName("connected")] + public bool Connected { get; set; } + + /// Up to 5 tool names with `_meta.ui` for quick inspection. + [JsonPropertyName("sampleToolNames")] + public IList SampleToolNames { get => field ??= []; set; } + + /// Total tools returned by the server's tools/list. + [JsonPropertyName("toolCount")] + public double ToolCount { get; set; } + + /// Tools whose `_meta.ui` is populated (resourceUri and/or visibility set). + [JsonPropertyName("toolsWithUiMeta")] + public double ToolsWithUiMeta { get; set; } +} + +/// Diagnostic snapshot of MCP Apps wiring for the named server. +[Experimental(Diagnostics.Experimental)] +public sealed class McpAppsDiagnoseResult +{ + /// Capability negotiation snapshot. + [JsonPropertyName("capability")] + public McpAppsDiagnoseCapability Capability { get => field ??= new(); set; } + + /// What the server returned for this session. + [JsonPropertyName("server")] + public McpAppsDiagnoseServer Server { get => field ??= new(); set; } +} + +/// MCP server to diagnose MCP Apps wiring for. +[Experimental(Diagnostics.Experimental)] +internal sealed class McpAppsDiagnoseRequest +{ + /// MCP server to probe. + [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")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// MCP resource content with URI, optional MIME type, text or base64 blob, and resource metadata. +[Experimental(Diagnostics.Experimental)] +public sealed class McpResourceContent +{ + /// Resource-level metadata (CSP, permissions, etc.). + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } + + /// Base64-encoded binary content. + [JsonPropertyName("blob")] + public string? Blob { get; set; } + + /// MIME type of the content. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } + + /// Text content (e.g. HTML). + [JsonPropertyName("text")] + public string? Text { get; set; } + + /// The resource URI. + [JsonPropertyName("uri")] + public string Uri { get; set; } = string.Empty; +} + +/// Resource contents returned by the MCP server. +[Experimental(Diagnostics.Experimental)] +public sealed class McpResourcesReadResult +{ + /// Resource contents returned by the server. + [JsonPropertyName("contents")] + public IList Contents { get => field ??= []; set; } +} + +/// MCP server and resource URI to fetch. +[Experimental(Diagnostics.Experimental)] +internal sealed class McpResourcesReadRequest +{ + /// Name of the MCP server hosting the resource. + [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")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Resource URI. + [JsonPropertyName("uri")] + public string Uri { get; set; } = string.Empty; +} + +/// Standard MCP resource annotations plus preserved non-standard annotation fields. +[Experimental(Diagnostics.Experimental)] +public sealed class McpResourceAnnotations +{ + /// Server-provided non-standard annotation fields preserved from the MCP response. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } + + /// Intended audience roles for this resource. + [JsonPropertyName("audience")] + public IList? Audience { get; set; } + + /// Last-modified timestamp hint. + [JsonPropertyName("lastModified")] + public string? LastModified { get; set; } + + /// Priority hint for model/client use. + [JsonPropertyName("priority")] + public double? Priority { get; set; } +} + +/// A resource icon descriptor plus preserved non-standard icon fields. +[Experimental(Diagnostics.Experimental)] +public sealed class McpResourceIcon +{ + /// Server-provided non-standard icon fields preserved from the MCP response. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } + + /// Icon MIME type, when known. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } + + /// Icon sizes hint. + [JsonPropertyName("sizes")] + public string? Sizes { get; set; } + + /// Icon URI. + [JsonPropertyName("src")] + public string Src { get; set; } = string.Empty; + + /// Theme hint for this icon. + [JsonPropertyName("theme")] + public string? Theme { get; set; } +} + +/// An MCP resource descriptor (spec `Resource`): URI, name, and optional title, description, MIME type, size, icons, annotations, and metadata. Server-provided fields outside the standard descriptor shape are exposed under `additionalProperties`. +[Experimental(Diagnostics.Experimental)] +public sealed class McpResource +{ + /// Resource-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } + + /// Server-provided non-standard descriptor fields preserved from the MCP response. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } + + /// Model/client annotations associated with this resource. + [JsonPropertyName("annotations")] + public McpResourceAnnotations? Annotations { get; set; } + + /// Optional description of what this resource represents. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Icons associated with this resource. + [JsonPropertyName("icons")] + public IList? Icons { get; set; } + + /// MIME type of the resource, if known. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } + + /// The programmatic name of the resource. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Resource size in bytes, when known. + [JsonPropertyName("size")] + public long? Size { get; set; } + + /// Optional human-readable display title. + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// The resource URI (e.g. ui://... or file:///...). + [JsonPropertyName("uri")] + public string Uri { get; set; } = string.Empty; +} + +/// One page of resources advertised by the named MCP server. +[Experimental(Diagnostics.Experimental)] +public sealed class McpResourcesListResult +{ + /// Opaque cursor for the next page, if the server has more resources. + [JsonPropertyName("nextCursor")] + public string? NextCursor { get; set; } + + /// Resources advertised by the server (proxied MCP `resources/list`). + [JsonPropertyName("resources")] + public IList Resources { get => field ??= []; set; } +} + +/// MCP server whose resources to enumerate. +[Experimental(Diagnostics.Experimental)] +internal sealed class McpResourcesListRequest +{ + /// Opaque MCP pagination cursor from a prior `nextCursor` value. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } + + /// Name of the MCP server whose resources to enumerate. + [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")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// An MCP resource template descriptor (spec `ResourceTemplate`): an RFC 6570 URI template, name, and optional title, description, MIME type, icons, annotations, and metadata. Server-provided fields outside the standard descriptor shape are exposed under `additionalProperties`. +[Experimental(Diagnostics.Experimental)] +public sealed class McpResourceTemplate +{ + /// Resource-template-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } + + /// Server-provided non-standard descriptor fields preserved from the MCP response. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } + + /// Model/client annotations associated with this template. + [JsonPropertyName("annotations")] + public McpResourceAnnotations? Annotations { get; set; } + + /// Optional description of what this template is for. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Icons associated with resources matching this template. + [JsonPropertyName("icons")] + public IList? Icons { get; set; } -/// What the server returned for this session. -[Experimental(Diagnostics.Experimental)] -public sealed class McpAppsDiagnoseServer -{ - /// Whether the named server is currently connected. - [JsonPropertyName("connected")] - public bool Connected { get; set; } + /// MIME type for resources matching this template, if uniform. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } - /// Up to 5 tool names with `_meta.ui` for quick inspection. - [JsonPropertyName("sampleToolNames")] - public IList SampleToolNames { get => field ??= []; set; } + /// The programmatic name of the resource template. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Total tools returned by the server's tools/list. - [JsonPropertyName("toolCount")] - public double ToolCount { get; set; } + /// Optional human-readable display title. + [JsonPropertyName("title")] + public string? Title { get; set; } - /// Tools whose `_meta.ui` is populated (resourceUri and/or visibility set). - [JsonPropertyName("toolsWithUiMeta")] - public double ToolsWithUiMeta { get; set; } + /// An RFC 6570 URI template for constructing resource URIs. + [JsonPropertyName("uriTemplate")] + public string UriTemplate { get; set; } = string.Empty; } -/// Diagnostic snapshot of MCP Apps wiring for the named server. +/// One page of resource templates advertised by the named MCP server. [Experimental(Diagnostics.Experimental)] -public sealed class McpAppsDiagnoseResult +public sealed class McpResourcesListTemplatesResult { - /// Capability negotiation snapshot. - [JsonPropertyName("capability")] - public McpAppsDiagnoseCapability Capability { get => field ??= new(); set; } + /// Opaque cursor for the next page, if the server has more resource templates. + [JsonPropertyName("nextCursor")] + public string? NextCursor { get; set; } - /// What the server returned for this session. - [JsonPropertyName("server")] - public McpAppsDiagnoseServer Server { get => field ??= new(); set; } + /// Resource templates advertised by the server (proxied MCP `resources/templates/list`). + [JsonPropertyName("resourceTemplates")] + public IList ResourceTemplates { get => field ??= []; set; } } -/// MCP server to diagnose MCP Apps wiring for. +/// MCP server whose resource templates to enumerate. [Experimental(Diagnostics.Experimental)] -internal sealed class McpAppsDiagnoseRequest +internal sealed class McpResourcesListTemplatesRequest { - /// MCP server to probe. + /// Opaque MCP pagination cursor from a prior `nextCursor` value. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } + + /// Name of the MCP server whose resource templates to enumerate. [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")] [MinLength(1)] @@ -6428,6 +6925,10 @@ public sealed class PluginsReloadRequest [JsonPropertyName("reloadCustomAgents")] public bool? ReloadCustomAgents { get; set; } + /// Re-discover and relaunch subprocess extensions (including plugin-shipped extensions) after refreshing plugins. Defaults to true. Has no effect when the session has no active extension controller (e.g. extensions were not requested for the session). + [JsonPropertyName("reloadExtensions")] + public bool? ReloadExtensions { get; set; } + /// Re-load user, plugin, and (subject to `deferRepoHooks`) repo hooks. Defaults to true. Has no effect when the host has not registered a hook reloader (e.g. remote sessions). [JsonPropertyName("reloadHooks")] public bool? ReloadHooks { get; set; } @@ -6449,6 +6950,10 @@ internal sealed class PluginsReloadRequestWithSession [JsonPropertyName("reloadCustomAgents")] public bool? ReloadCustomAgents { get; set; } + /// Re-discover and relaunch subprocess extensions (including plugin-shipped extensions) after refreshing plugins. Defaults to true. Has no effect when the session has no active extension controller (e.g. extensions were not requested for the session). + [JsonPropertyName("reloadExtensions")] + public bool? ReloadExtensions { get; set; } + /// Re-load user, plugin, and (subject to `deferRepoHooks`) repo hooks. Defaults to true. Has no effect when the host has not registered a hook reloader (e.g. remote sessions). [JsonPropertyName("reloadHooks")] public bool? ReloadHooks { get; set; } @@ -6665,6 +7170,10 @@ internal sealed class ProviderAddRequest [Experimental(Diagnostics.Experimental)] public sealed class SessionUpdateOptionsResult { + /// Number of hooks loaded from installed plugins, returned when installedPlugins is updated. + [JsonPropertyName("pluginHookCount")] + public long? PluginHookCount { get; set; } + /// Whether the operation succeeded. [JsonPropertyName("success")] public bool Success { get; set; } @@ -7036,6 +7545,10 @@ internal sealed class SessionUpdateOptionsParams [JsonPropertyName("featureFlags")] public IDictionary? FeatureFlags { get; set; } + /// 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. Set to null to remove the allowlist restriction. + [JsonPropertyName("includedBuiltinAgents")] + public IList? IncludedBuiltinAgents { get; set; } + /// Full set of installed plugins for the session. Replaces the existing list; the runtime invalidates the skills cache only when the list materially changes. [JsonPropertyName("installedPlugins")] public IList? InstalledPlugins { get; set; } @@ -7140,6 +7653,10 @@ internal sealed class SessionUpdateOptionsParams [JsonPropertyName("trajectoryFile")] public string? TrajectoryFile { get; set; } + /// Output verbosity level for supported models. + [JsonPropertyName("verbosity")] + public Verbosity? Verbosity { get; set; } + /// Absolute working-directory path for shell tools. [JsonPropertyName("workingDirectory")] public string? WorkingDirectory { get; set; } @@ -7913,90 +8430,6 @@ internal sealed class UpdateSubagentSettingsRequest public UpdateSubagentSettingsRequestSubagents? Subagents { get; set; } } -/// A literal choice the command input accepts, with a human-facing description. -[Experimental(Diagnostics.Experimental)] -public sealed class SlashCommandInputChoice -{ - /// Human-readable description shown alongside the choice. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; - - /// The literal choice value (e.g. 'on', 'off', 'show'). - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; -} - -/// Optional unstructured input hint. -[Experimental(Diagnostics.Experimental)] -public sealed class SlashCommandInput -{ - /// Optional literal choices the input accepts, each with a human-facing description; clients may render these as selectable options. - [JsonPropertyName("choices")] - public IList? Choices { get; set; } - - /// Optional completion hint for the input (e.g. 'directory' for filesystem path completion). - [JsonPropertyName("completion")] - public SlashCommandInputCompletion? Completion { get; set; } - - /// Hint to display when command input has not been provided. - [JsonPropertyName("hint")] - public string Hint { get; set; } = string.Empty; - - /// When true, clients should pass the full text after the command name as a single argument rather than splitting on whitespace. - [JsonPropertyName("preserveMultilineInput")] - public bool? PreserveMultilineInput { get; set; } - - /// When true, the command requires non-empty input; clients should render the input hint as required. - [JsonPropertyName("required")] - public bool? Required { get; set; } -} - -/// Slash-command metadata with name, aliases, description, kind, input hint, execution allowance, and schedulability. -[Experimental(Diagnostics.Experimental)] -public sealed class SlashCommandInfo -{ - /// Canonical aliases without leading slashes. - [JsonPropertyName("aliases")] - public IList? Aliases { get; set; } - - /// Whether the command may run while an agent turn is active. - [JsonPropertyName("allowDuringAgentExecution")] - public bool AllowDuringAgentExecution { get; set; } - - /// Human-readable command description. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; - - /// Whether the command is experimental. - [JsonPropertyName("experimental")] - public bool? Experimental { get; set; } - - /// Optional unstructured input hint. - [JsonPropertyName("input")] - public SlashCommandInput? Input { get; set; } - - /// Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command. - [JsonPropertyName("kind")] - public SlashCommandKind Kind { get; set; } - - /// Canonical command name without a leading slash. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// Whether the command may be the target of `/every` / `/after` schedules. Resolution happens at every tick, so only set this when the command is safe to re-invoke and produces an agent prompt. - [JsonPropertyName("schedulable")] - public bool? Schedulable { get; set; } -} - -/// Slash commands available in the session, after applying any include/exclude filters. -[Experimental(Diagnostics.Experimental)] -public sealed class CommandList -{ - /// Commands available in this session. - [JsonPropertyName("commands")] - public IList Commands { get => field ??= []; set; } -} - /// Optional filters controlling which command sources to include in the listing. [Experimental(Diagnostics.Experimental)] public sealed class CommandsListRequest @@ -8082,7 +8515,7 @@ public partial class SlashCommandInvocationResultText : SlashCommandInvocationRe public required string Text { get; set; } } -/// Slash-command invocation result that submits an agent prompt, with display prompt, optional mode, and settings-change flag. +/// Slash-command invocation result that submits an agent prompt, with display prompt, optional mode, optional user-facing notice, and settings-change flag. /// The agent-prompt variant of . [Experimental(Diagnostics.Experimental)] public partial class SlashCommandInvocationResultAgentPrompt : SlashCommandInvocationResult @@ -8100,6 +8533,11 @@ public partial class SlashCommandInvocationResultAgentPrompt : SlashCommandInvoc [JsonPropertyName("mode")] public SessionMode? Mode { get; set; } + /// Optional user-facing notice to show before the prompt is submitted. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("notice")] + public string? Notice { get; set; } + /// Prompt to submit to the agent. [JsonPropertyName("prompt")] public required string Prompt { get; set; } @@ -10412,7 +10850,7 @@ internal sealed class MetadataRecordContextChangeRequest public string SessionId { get; set; } = string.Empty; } -/// Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for `process.chdir` and any related side-effects (file index, etc.); this method only updates the session's own recorded path. +/// Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects (file index, etc.); it does NOT change the process working directory (a session's cwd is per-session, not process-global). For local sessions the runtime validates the target first (an absolute path that exists on disk) and re-bases the permission primary directory; a rejected validation fails the call before anything is mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the new directory (best-effort). Remote sessions only record the path. [Experimental(Diagnostics.Experimental)] public sealed class MetadataSetWorkingDirectoryResult { @@ -10421,7 +10859,7 @@ public sealed class MetadataSetWorkingDirectoryResult public string WorkingDirectory { get; set; } = string.Empty; } -/// Absolute path to set as the session's new working directory. +/// Absolute path to set as the session's new working directory. For local sessions the path must be absolute and exist on disk: it is validated before any session state changes, and a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote sessions record the path as-is. [Experimental(Diagnostics.Experimental)] internal sealed class MetadataSetWorkingDirectoryRequest { @@ -11119,7 +11557,7 @@ public sealed class RegisterEventInterestResult [Experimental(Diagnostics.Experimental)] internal sealed class RegisterEventInterestParams { - /// The event type the consumer wants the runtime to treat as 'observed' for behavior-switching gating. Some runtime code paths inspect whether any consumer is interested in a specific event type and choose a different implementation accordingly (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates the full interactive OAuth flow to the consumer; when no interest is registered the runtime installs a browserless fallback that silently reuses cached tokens). SDK clients that long-poll events do NOT automatically appear as listeners to these gating checks — they must explicitly call `registerInterest` for each event type they want the runtime to count as having a consumer. Multiple registrations for the same event type from the same or different consumers are tracked independently and must each be released. See: `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, `session_limits_exhausted.requested`, `user_input.requested`, `elicitation.requested`, `command.queued`, `exit_plan_mode.requested`. + /// The event type the consumer wants the runtime to treat as 'observed' for behavior-switching gating. Some runtime code paths inspect whether any consumer is interested in a specific event type and choose a different implementation accordingly (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates interactive OAuth token acquisition to the consumer via `mcp.oauth_required` events; when no interest is registered the runtime still attempts non-interactive reconnect from cached or refreshable tokens, and only marks the server `needs-auth` if usable credentials are unavailable — it does not open a browser or start interactive OAuth without a consumer). SDK clients that long-poll events do NOT automatically appear as listeners to these gating checks — they must explicitly call `registerInterest` for each event type they want the runtime to count as having a consumer. Multiple registrations for the same event type from the same or different consumers are tracked independently and must each be released. See: `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, `session_limits_exhausted.requested`, `user_input.requested`, `elicitation.requested`, `command.queued`, `exit_plan_mode.requested`. [JsonPropertyName("eventType")] public string EventType { get; set; } = string.Empty; @@ -12983,14 +13421,140 @@ public InstructionDiscoveryPathLocation(string value) /// Returns a value indicating whether two instances are equivalent. public static bool operator ==(InstructionDiscoveryPathLocation left, InstructionDiscoveryPathLocation right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(InstructionDiscoveryPathLocation left, InstructionDiscoveryPathLocation right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(InstructionDiscoveryPathLocation left, InstructionDiscoveryPathLocation right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is InstructionDiscoveryPathLocation other && Equals(other); + + /// + public bool Equals(InstructionDiscoveryPathLocation 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 InstructionDiscoveryPathLocation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, InstructionDiscoveryPathLocation value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionDiscoveryPathLocation)); + } + } +} + + +/// Optional completion hint for the input (e.g. 'directory' for filesystem path completion). +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct SlashCommandInputCompletion : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public SlashCommandInputCompletion(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Input should complete filesystem directories. + public static SlashCommandInputCompletion Directory { get; } = new("directory"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SlashCommandInputCompletion left, SlashCommandInputCompletion right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SlashCommandInputCompletion left, SlashCommandInputCompletion right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is SlashCommandInputCompletion other && Equals(other); + + /// + public bool Equals(SlashCommandInputCompletion 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 SlashCommandInputCompletion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, SlashCommandInputCompletion value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandInputCompletion)); + } + } +} + + +/// Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct SlashCommandKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public SlashCommandKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Command implemented by the runtime. + public static SlashCommandKind Builtin { get; } = new("builtin"); + + /// Command backed by a skill. + public static SlashCommandKind Skill { get; } = new("skill"); + + /// Command registered by an SDK client or extension. + public static SlashCommandKind Client { get; } = new("client"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SlashCommandKind left, SlashCommandKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SlashCommandKind left, SlashCommandKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is InstructionDiscoveryPathLocation other && Equals(other); + public override bool Equals(object? obj) => obj is SlashCommandKind other && Equals(other); /// - public bool Equals(InstructionDiscoveryPathLocation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SlashCommandKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -12998,20 +13562,20 @@ public InstructionDiscoveryPathLocation(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 InstructionDiscoveryPathLocation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SlashCommandKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, InstructionDiscoveryPathLocation value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SlashCommandKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionDiscoveryPathLocation)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandKind)); } } } @@ -15018,6 +15582,69 @@ public override void Write(Utf8JsonWriter writer, TaskShellInfoAttachmentMode va } +/// Consumer allowed to call an MCP tool. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpToolUiVisibility : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpToolUiVisibility(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The model may call the tool. + public static McpToolUiVisibility Model { get; } = new("model"); + + /// An MCP App view may call the tool. + public static McpToolUiVisibility App { get; } = new("app"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpToolUiVisibility left, McpToolUiVisibility right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpToolUiVisibility left, McpToolUiVisibility right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is McpToolUiVisibility other && Equals(other); + + /// + public bool Equals(McpToolUiVisibility 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 McpToolUiVisibility Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, McpToolUiVisibility value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpToolUiVisibility)); + } + } +} + + /// Outcome of the sampling inference. 'success' produced a response; 'failure' encountered an error (including agent-side rejection by content filter or criteria); 'cancelled' the caller cancelled this execution via cancelSamplingExecution. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -16794,132 +17421,6 @@ public override void Write(Utf8JsonWriter writer, SubagentSettingsEntryContextTi } -/// Optional completion hint for the input (e.g. 'directory' for filesystem path completion). -[Experimental(Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct SlashCommandInputCompletion : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public SlashCommandInputCompletion(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// Input should complete filesystem directories. - public static SlashCommandInputCompletion Directory { get; } = new("directory"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SlashCommandInputCompletion left, SlashCommandInputCompletion right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SlashCommandInputCompletion left, SlashCommandInputCompletion right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is SlashCommandInputCompletion other && Equals(other); - - /// - public bool Equals(SlashCommandInputCompletion 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 SlashCommandInputCompletion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, SlashCommandInputCompletion value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandInputCompletion)); - } - } -} - - -/// Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command. -[Experimental(Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct SlashCommandKind : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public SlashCommandKind(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// Command implemented by the runtime. - public static SlashCommandKind Builtin { get; } = new("builtin"); - - /// Command backed by a skill. - public static SlashCommandKind Skill { get; } = new("skill"); - - /// Command registered by an SDK client or extension. - public static SlashCommandKind Client { get; } = new("client"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SlashCommandKind left, SlashCommandKind right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SlashCommandKind left, SlashCommandKind right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is SlashCommandKind other && Equals(other); - - /// - public bool Equals(SlashCommandKind 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 SlashCommandKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, SlashCommandKind value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandKind)); - } - } -} - - /// The user's response: accept (submitted), decline (rejected), or cancel (dismissed). [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -18678,6 +19179,12 @@ internal async Task ConnectAsync(string? token = null, bool? enab Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; + /// Commands APIs. + public ServerCommandsApi Commands => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + /// User APIs. public ServerUserApi User => field ?? @@ -19077,13 +19584,14 @@ public async Task ListAsync(CancellationToken cancellatio /// Registers a new marketplace from a source (owner/repo, URL, or local path). /// Marketplace source. Accepts the same forms as the CLI: "owner/repo" or "owner/repo#ref" (GitHub), an http/https/ssh URL (optionally with #ref), a git scp-style URL (user@host:path), or a local path. The marketplace's own name (from its manifest) is used as the registration key. + /// Working directory used to resolve relative local paths in `source`. Defaults to the server's current working directory. /// The to monitor for cancellation requests. The default is . /// Result of registering a new marketplace. - public async Task AddAsync(string source, CancellationToken cancellationToken = default) + public async Task AddAsync(string source, string? workingDirectory = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(source); - var request = new PluginsMarketplacesAddRequest { Source = source }; + var request = new PluginsMarketplacesAddRequest { Source = source, WorkingDirectory = workingDirectory }; return await CopilotClient.InvokeRpcAsync(_rpc, "plugins.marketplaces.add", [request], cancellationToken); } @@ -19255,6 +19763,26 @@ public async Task GetDiscoveryPathsAsync(IListProvides server-scoped Commands APIs. +[Experimental(Diagnostics.Experimental)] +public sealed class ServerCommandsApi +{ + private readonly JsonRpc _rpc; + + internal ServerCommandsApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Lists the well-known built-in slash commands that work as the first message in a new session (e.g. /plan, /env), without requiring an active session. Commands that depend on session state, authentication, or a synced session are omitted. + /// The to monitor for cancellation requests. The default is . + /// Slash commands available in the session, after applying any include/exclude filters. + public async Task ListAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "commands.list", [], cancellationToken); + } +} + /// Provides server-scoped User APIs. [Experimental(Diagnostics.Experimental)] public sealed class ServerUserApi @@ -20043,6 +20571,27 @@ public async Task SendAsync(string prompt, string? displayPrompt = n return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.send", [request], cancellationToken); } + /// Sends zero or more user messages to the session in a single turn and returns their message IDs. All provided messages are appended to the conversation in order, then exactly one agent turn runs over the resulting history. When the list is empty, one turn runs over the existing history with no new user message. Remote-backed (Mission Control) sessions do not support this method and will return an error. + /// The user messages to append to the conversation, in order. May be empty, in which case a single turn runs over the existing history with no new user message. + /// How to deliver the messages. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. + /// If true, adds the messages to the front of the queue instead of the end. + /// The UI mode the agent was in when these messages were sent. Defaults to the session's current mode. + /// Custom HTTP headers to include in outbound model requests for this turn. Merged with session-level provider headers; per-turn headers augment and overwrite session-level headers with the same key. + /// W3C Trace Context traceparent header for distributed tracing of this agent turn. + /// W3C Trace Context tracestate header for distributed tracing. + /// If true, await completion of the agentic loop for this turn before returning. Defaults to false (fire-and-forget). When true, the result still contains the same `messageIds`; the caller can rely on the agent having processed the messages before the call resolves. + /// The to monitor for cancellation requests. The default is . + /// Result of sending zero or more user messages. + [Experimental(Diagnostics.Experimental)] + public async Task SendMessagesAsync(IList messages, SendMode? mode = null, bool? prepend = null, SendAgentMode? agentMode = null, IDictionary? requestHeaders = null, string? traceparent = null, string? tracestate = null, bool? wait = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(messages); + _session.ThrowIfDisposed(); + + var request = new SendMessagesRequest { SessionId = _session.SessionId, Messages = messages, Mode = mode, Prepend = prepend, AgentMode = agentMode, RequestHeaders = requestHeaders, Traceparent = traceparent, Tracestate = tracestate, Wait = wait }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.sendMessages", [request], cancellationToken); + } + /// Aborts the current agent turn. /// Finite reason code describing why the current turn was aborted. /// The to monitor for cancellation requests. The default is . @@ -20274,16 +20823,17 @@ public async Task GetCurrentAsync(CancellationToken cancellationTo /// 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. /// Reasoning effort level to use for the model. "none" disables reasoning. /// 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. /// 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, ModelCapabilitiesOverride? modelCapabilities = null, ContextTier? contextTier = null, CancellationToken cancellationToken = default) + public async Task SwitchToAsync(string modelId, string? reasoningEffort = null, ReasoningSummary? reasoningSummary = null, Verbosity? verbosity = null, ModelCapabilitiesOverride? modelCapabilities = null, ContextTier? contextTier = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(modelId); _session.ThrowIfDisposed(); - var request = new ModelSwitchToRequest { SessionId = _session.SessionId, ModelId = modelId, ReasoningEffort = reasoningEffort, ReasoningSummary = reasoningSummary, ModelCapabilities = modelCapabilities, ContextTier = contextTier }; + var request = new ModelSwitchToRequest { SessionId = _session.SessionId, ModelId = modelId, ReasoningEffort = reasoningEffort, ReasoningSummary = reasoningSummary, Verbosity = verbosity, ModelCapabilities = modelCapabilities, ContextTier = contextTier }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.model.switchTo", [request], cancellationToken); } @@ -20979,7 +21529,7 @@ public async Task ListAsync(CancellationToken cancellationToken = return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.list", [request], cancellationToken); } - /// Lists the tools exposed by a connected MCP server on this session's host. + /// Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session. /// Name of the connected MCP server whose tools to list. /// The to monitor for cancellation requests. The default is . /// Tools exposed by the connected MCP server. Throws when the server is not connected. @@ -21107,11 +21657,11 @@ internal async Task ConfigureGitHubAsync(object authIn return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.configureGitHub", [request], cancellationToken); } - /// Starts an individual MCP server on the session's host. + /// Starts an individual MCP server on the live session from a caller-supplied config. Session-scoped and ephemeral: the server is added to this session's running set only and is reaped when the session ends. Does NOT modify persistent user configuration (`mcp.config.*`), so it does not affect future sessions. The server surfaces through `session.mcp.list` and the `session.mcp_servers_loaded` / `session.mcp_server_status_changed` events like any other server. /// Name of the MCP server to start. - /// Opaque server configuration (MCPServerConfig). Marked internal: an in-process runtime shape supplied only by in-process CLI callers. + /// MCP server configuration (stdio process or remote HTTP/SSE). /// The to monitor for cancellation requests. The default is . - internal async Task StartServerAsync(string serverName, object config, CancellationToken cancellationToken = default) + public async Task StartServerAsync(string serverName, object config, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(serverName); ArgumentNullException.ThrowIfNull(config); @@ -21121,17 +21671,16 @@ internal async Task StartServerAsync(string serverName, object config, Cancellat await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.startServer", [request], cancellationToken); } - /// Restarts an individual MCP server on the session's host (stops then starts). + /// Restarts an individual MCP server on the live session (stops then starts). Omit `config` for a config-free restart-by-name of an already-configured server; supply `config` to restart with a replacement configuration. Session-scoped and ephemeral: does NOT modify persistent user configuration (`mcp.config.*`). /// Name of the MCP server to restart. - /// Opaque server configuration (MCPServerConfig). Marked internal: an in-process runtime shape supplied only by in-process CLI callers. + /// Replacement MCP server configuration (stdio process or remote HTTP/SSE). Omit to restart the server with its already-registered configuration (config-free restart-by-name). /// The to monitor for cancellation requests. The default is . - internal async Task RestartServerAsync(string serverName, object config, CancellationToken cancellationToken = default) + public async Task RestartServerAsync(string serverName, object? config = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(serverName); - ArgumentNullException.ThrowIfNull(config); _session.ThrowIfDisposed(); - var request = new McpRestartServerRequest { SessionId = _session.SessionId, ServerName = serverName, Config = CopilotClient.ToJsonElementForWire(config)!.Value }; + var request = new McpRestartServerRequest { SessionId = _session.SessionId, ServerName = serverName, Config = CopilotClient.ToJsonElementForWire(config) }; await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.restartServer", [request], cancellationToken); } @@ -21207,6 +21756,12 @@ public async Task IsServerRunningAsync(string serverNa field ?? Interlocked.CompareExchange(ref field, new(_session), null) ?? field; + + /// Resources APIs. + public McpResourcesApi Resources => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; } /// Provides session-scoped McpOauth APIs. @@ -21379,6 +21934,61 @@ public async Task DiagnoseAsync(string serverName, Cancel } } +/// Provides session-scoped McpResources APIs. +[Experimental(Diagnostics.Experimental)] +public sealed class McpResourcesApi +{ + private readonly CopilotSession _session; + + internal McpResourcesApi(CopilotSession session) + { + _session = session; + } + + /// Fetch an MCP resource from a connected server by URI (proxies MCP `resources/read`). + /// Name of the MCP server hosting the resource. + /// Resource URI. + /// The to monitor for cancellation requests. The default is . + /// Resource contents returned by the MCP server. + public async Task ReadAsync(string serverName, string uri, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(serverName); + ArgumentNullException.ThrowIfNull(uri); + _session.ThrowIfDisposed(); + + var request = new McpResourcesReadRequest { SessionId = _session.SessionId, ServerName = serverName, Uri = uri }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.resources.read", [request], cancellationToken); + } + + /// Enumerate one page of resources a connected MCP server exposes (proxies MCP `resources/list`). Pass `cursor` to continue from a prior result's `nextCursor`. + /// Name of the MCP server whose resources to enumerate. + /// Opaque MCP pagination cursor from a prior `nextCursor` value. + /// The to monitor for cancellation requests. The default is . + /// One page of resources advertised by the named MCP server. + public async Task ListAsync(string serverName, string? cursor = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(serverName); + _session.ThrowIfDisposed(); + + var request = new McpResourcesListRequest { SessionId = _session.SessionId, ServerName = serverName, Cursor = cursor }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.resources.list", [request], cancellationToken); + } + + /// Enumerate one page of resource templates a connected MCP server exposes (proxies MCP `resources/templates/list`). Pass `cursor` to continue from a prior result's `nextCursor`. + /// Name of the MCP server whose resource templates to enumerate. + /// Opaque MCP pagination cursor from a prior `nextCursor` value. + /// The to monitor for cancellation requests. The default is . + /// One page of resource templates advertised by the named MCP server. + public async Task ListTemplatesAsync(string serverName, string? cursor = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(serverName); + _session.ThrowIfDisposed(); + + var request = new McpResourcesListTemplatesRequest { SessionId = _session.SessionId, ServerName = serverName, Cursor = cursor }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.resources.listTemplates", [request], cancellationToken); + } +} + /// Provides session-scoped Plugins APIs. [Experimental(Diagnostics.Experimental)] public sealed class PluginsApi @@ -21408,7 +22018,7 @@ public async Task ReloadAsync(PluginsReloadRequest? request = null, Cancellation { _session.ThrowIfDisposed(); - var rpcRequest = new PluginsReloadRequestWithSession { SessionId = _session.SessionId, ReloadMcp = request?.ReloadMcp, ReloadCustomAgents = request?.ReloadCustomAgents, ReloadHooks = request?.ReloadHooks, DeferRepoHooks = request?.DeferRepoHooks }; + var rpcRequest = new PluginsReloadRequestWithSession { SessionId = _session.SessionId, ReloadMcp = request?.ReloadMcp, ReloadCustomAgents = request?.ReloadCustomAgents, ReloadHooks = request?.ReloadHooks, ReloadExtensions = request?.ReloadExtensions, DeferRepoHooks = request?.DeferRepoHooks }; await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.plugins.reload", [rpcRequest], cancellationToken); } } @@ -21466,6 +22076,7 @@ internal OptionsApi(CopilotSession session) /// Per-property model capability overrides for the selected model. /// Reasoning effort for the selected model (model-defined enum). /// Reasoning summary mode for supported model clients. + /// Output verbosity level for supported models. /// Identifier of the client driving the session. /// Identifier sent to LSP-style integrations. /// Stable integration identifier used for analytics and rate-limit attribution. @@ -21476,6 +22087,7 @@ internal OptionsApi(CopilotSession session) /// Absolute working-directory path for shell tools. /// Allowlist of tool names available to this session. /// Denylist of tool names for this session. + /// 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. Set to null to remove the allowlist restriction. /// Built-in subagent names to exclude from this session. Excluded built-ins are hidden from agent discovery and cannot be dispatched unless a custom agent with the same name is available. /// Controls how availableTools (allowlist) and excludedTools (denylist) combine when both are set. /// Whether shell-script safety heuristics are enabled. @@ -21517,11 +22129,11 @@ internal OptionsApi(CopilotSession session) /// Optional session limits. Pass null to clear the session limits. /// The to monitor for cancellation requests. The default is . /// Indicates whether the session options patch was applied successfully. - public async Task UpdateAsync(string? model = null, ModelCapabilitiesOverride? modelCapabilitiesOverrides = null, string? reasoningEffort = null, OptionsUpdateReasoningSummary? reasoningSummary = null, string? clientName = null, string? lspClientName = null, string? integrationId = null, IDictionary? featureFlags = null, bool? isExperimentalMode = null, ProviderConfig? provider = null, CapiSessionOptions? capi = null, string? workingDirectory = null, IList? availableTools = null, IList? excludedTools = null, IList? excludedBuiltinAgents = null, OptionsUpdateToolFilterPrecedence? toolFilterPrecedence = null, bool? enableScriptSafety = null, string? shellInitProfile = null, IList? shellProcessFlags = null, SandboxConfig? sandboxConfig = null, bool? logInteractiveShells = null, OptionsUpdateEnvValueMode? envValueMode = null, bool? allowAllMcpServerInstructions = null, IList? skillDirectories = null, IList? disabledSkills = null, bool? enableOnDemandInstructionDiscovery = null, long? maxInlineBinaryBytes = null, IList? installedPlugins = null, bool? customAgentsLocalOnly = null, bool? suppressCustomAgentPrompt = null, bool? skipCustomInstructions = null, IList? disabledInstructionSources = null, bool? coauthorEnabled = null, string? trajectoryFile = null, bool? enableStreaming = null, string? copilotUrl = null, bool? askUserDisabled = null, bool? continueOnAutoMode = null, bool? runningInInteractiveMode = null, bool? enableReasoningSummaries = null, string? agentContext = null, string? eventsLogDirectory = null, IList? additionalContentExclusionPolicies = null, bool? manageScheduleEnabled = null, IList? sessionCapabilities = null, bool? skipEmbeddingRetrieval = null, string? organizationCustomInstructions = null, bool? enableFileHooks = null, bool? enableHostGitOperations = null, bool? enableSessionStore = null, bool? enableSkills = null, OptionsUpdateContextTier? contextTier = null, SessionLimitsConfig? sessionLimits = null, CancellationToken cancellationToken = default) + public async Task UpdateAsync(string? model = null, ModelCapabilitiesOverride? modelCapabilitiesOverrides = null, string? reasoningEffort = null, OptionsUpdateReasoningSummary? reasoningSummary = null, Verbosity? verbosity = null, string? clientName = null, string? lspClientName = null, string? integrationId = null, IDictionary? featureFlags = null, bool? isExperimentalMode = null, ProviderConfig? provider = null, CapiSessionOptions? capi = null, string? workingDirectory = null, IList? availableTools = null, IList? excludedTools = null, IList? includedBuiltinAgents = null, IList? excludedBuiltinAgents = null, OptionsUpdateToolFilterPrecedence? toolFilterPrecedence = null, bool? enableScriptSafety = null, string? shellInitProfile = null, IList? shellProcessFlags = null, SandboxConfig? sandboxConfig = null, bool? logInteractiveShells = null, OptionsUpdateEnvValueMode? envValueMode = null, bool? allowAllMcpServerInstructions = null, IList? skillDirectories = null, IList? disabledSkills = null, bool? enableOnDemandInstructionDiscovery = null, long? maxInlineBinaryBytes = null, IList? installedPlugins = null, bool? customAgentsLocalOnly = null, bool? suppressCustomAgentPrompt = null, bool? skipCustomInstructions = null, IList? disabledInstructionSources = null, bool? coauthorEnabled = null, string? trajectoryFile = null, bool? enableStreaming = null, string? copilotUrl = null, bool? askUserDisabled = null, bool? continueOnAutoMode = null, bool? runningInInteractiveMode = null, bool? enableReasoningSummaries = null, string? agentContext = null, string? eventsLogDirectory = null, IList? additionalContentExclusionPolicies = null, bool? manageScheduleEnabled = null, IList? sessionCapabilities = null, bool? skipEmbeddingRetrieval = null, string? organizationCustomInstructions = null, bool? enableFileHooks = null, bool? enableHostGitOperations = null, bool? enableSessionStore = null, bool? enableSkills = null, OptionsUpdateContextTier? contextTier = null, SessionLimitsConfig? sessionLimits = null, CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); - var request = new SessionUpdateOptionsParams { SessionId = _session.SessionId, Model = model, ModelCapabilitiesOverrides = modelCapabilitiesOverrides, ReasoningEffort = reasoningEffort, ReasoningSummary = reasoningSummary, ClientName = clientName, LspClientName = lspClientName, IntegrationId = integrationId, FeatureFlags = featureFlags, IsExperimentalMode = isExperimentalMode, Provider = provider, Capi = capi, WorkingDirectory = workingDirectory, AvailableTools = availableTools, ExcludedTools = excludedTools, ExcludedBuiltinAgents = excludedBuiltinAgents, ToolFilterPrecedence = toolFilterPrecedence, EnableScriptSafety = enableScriptSafety, ShellInitProfile = shellInitProfile, ShellProcessFlags = shellProcessFlags, SandboxConfig = sandboxConfig, LogInteractiveShells = logInteractiveShells, EnvValueMode = envValueMode, AllowAllMcpServerInstructions = allowAllMcpServerInstructions, SkillDirectories = skillDirectories, DisabledSkills = disabledSkills, EnableOnDemandInstructionDiscovery = enableOnDemandInstructionDiscovery, MaxInlineBinaryBytes = maxInlineBinaryBytes, InstalledPlugins = installedPlugins, CustomAgentsLocalOnly = customAgentsLocalOnly, SuppressCustomAgentPrompt = suppressCustomAgentPrompt, SkipCustomInstructions = skipCustomInstructions, DisabledInstructionSources = disabledInstructionSources, CoauthorEnabled = coauthorEnabled, TrajectoryFile = trajectoryFile, EnableStreaming = enableStreaming, CopilotUrl = copilotUrl, AskUserDisabled = askUserDisabled, ContinueOnAutoMode = continueOnAutoMode, RunningInInteractiveMode = runningInInteractiveMode, EnableReasoningSummaries = enableReasoningSummaries, AgentContext = agentContext, EventsLogDirectory = eventsLogDirectory, AdditionalContentExclusionPolicies = additionalContentExclusionPolicies, ManageScheduleEnabled = manageScheduleEnabled, SessionCapabilities = sessionCapabilities, SkipEmbeddingRetrieval = skipEmbeddingRetrieval, OrganizationCustomInstructions = organizationCustomInstructions, EnableFileHooks = enableFileHooks, EnableHostGitOperations = enableHostGitOperations, EnableSessionStore = enableSessionStore, EnableSkills = enableSkills, ContextTier = contextTier, SessionLimits = sessionLimits }; + var request = new SessionUpdateOptionsParams { SessionId = _session.SessionId, Model = model, ModelCapabilitiesOverrides = modelCapabilitiesOverrides, ReasoningEffort = reasoningEffort, ReasoningSummary = reasoningSummary, Verbosity = verbosity, ClientName = clientName, LspClientName = lspClientName, IntegrationId = integrationId, FeatureFlags = featureFlags, IsExperimentalMode = isExperimentalMode, Provider = provider, Capi = capi, WorkingDirectory = workingDirectory, AvailableTools = availableTools, ExcludedTools = excludedTools, IncludedBuiltinAgents = includedBuiltinAgents, ExcludedBuiltinAgents = excludedBuiltinAgents, ToolFilterPrecedence = toolFilterPrecedence, EnableScriptSafety = enableScriptSafety, ShellInitProfile = shellInitProfile, ShellProcessFlags = shellProcessFlags, SandboxConfig = sandboxConfig, LogInteractiveShells = logInteractiveShells, EnvValueMode = envValueMode, AllowAllMcpServerInstructions = allowAllMcpServerInstructions, SkillDirectories = skillDirectories, DisabledSkills = disabledSkills, EnableOnDemandInstructionDiscovery = enableOnDemandInstructionDiscovery, MaxInlineBinaryBytes = maxInlineBinaryBytes, InstalledPlugins = installedPlugins, CustomAgentsLocalOnly = customAgentsLocalOnly, SuppressCustomAgentPrompt = suppressCustomAgentPrompt, SkipCustomInstructions = skipCustomInstructions, DisabledInstructionSources = disabledInstructionSources, CoauthorEnabled = coauthorEnabled, TrajectoryFile = trajectoryFile, EnableStreaming = enableStreaming, CopilotUrl = copilotUrl, AskUserDisabled = askUserDisabled, ContinueOnAutoMode = continueOnAutoMode, RunningInInteractiveMode = runningInInteractiveMode, EnableReasoningSummaries = enableReasoningSummaries, AgentContext = agentContext, EventsLogDirectory = eventsLogDirectory, AdditionalContentExclusionPolicies = additionalContentExclusionPolicies, ManageScheduleEnabled = manageScheduleEnabled, SessionCapabilities = sessionCapabilities, SkipEmbeddingRetrieval = skipEmbeddingRetrieval, OrganizationCustomInstructions = organizationCustomInstructions, EnableFileHooks = enableFileHooks, EnableHostGitOperations = enableHostGitOperations, EnableSessionStore = enableSessionStore, EnableSkills = enableSkills, ContextTier = contextTier, SessionLimits = sessionLimits }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.options.update", [request], cancellationToken); } } @@ -22419,10 +23031,10 @@ public async Task RecordContextChangeAsync(Se return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.metadata.recordContextChange", [request], cancellationToken); } - /// Updates the session's recorded working directory. + /// Updates the session's working directory. For local sessions the target is validated first (an absolute path that exists on disk) and the permission primary directory is re-based; a rejected validation fails the call before any session state changes. /// Absolute path to set as the session's working directory. The runtime updates the session's recorded cwd so subsequent operations (shell tools, file lookups, telemetry) anchor to it. /// The to monitor for cancellation requests. The default is . - /// Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for `process.chdir` and any related side-effects (file index, etc.); this method only updates the session's own recorded path. + /// Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects (file index, etc.); it does NOT change the process working directory (a session's cwd is per-session, not process-global). For local sessions the runtime validates the target first (an absolute path that exists on disk) and re-bases the permission primary directory; a rejected validation fails the call before anything is mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the new directory (best-effort). Remote sessions only record the path. public async Task SetWorkingDirectoryAsync(string workingDirectory, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(workingDirectory); @@ -22704,7 +23316,7 @@ public async Task TailAsync(CancellationToken cancellationTo } /// Registers consumer interest in an event type for runtime gating purposes. - /// The event type the consumer wants the runtime to treat as 'observed' for behavior-switching gating. Some runtime code paths inspect whether any consumer is interested in a specific event type and choose a different implementation accordingly (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates the full interactive OAuth flow to the consumer; when no interest is registered the runtime installs a browserless fallback that silently reuses cached tokens). SDK clients that long-poll events do NOT automatically appear as listeners to these gating checks — they must explicitly call `registerInterest` for each event type they want the runtime to count as having a consumer. Multiple registrations for the same event type from the same or different consumers are tracked independently and must each be released. See: `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, `session_limits_exhausted.requested`, `user_input.requested`, `elicitation.requested`, `command.queued`, `exit_plan_mode.requested`. + /// The event type the consumer wants the runtime to treat as 'observed' for behavior-switching gating. Some runtime code paths inspect whether any consumer is interested in a specific event type and choose a different implementation accordingly (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates interactive OAuth token acquisition to the consumer via `mcp.oauth_required` events; when no interest is registered the runtime still attempts non-interactive reconnect from cached or refreshable tokens, and only marks the server `needs-auth` if usable credentials are unavailable — it does not open a browser or start interactive OAuth without a consumer). SDK clients that long-poll events do NOT automatically appear as listeners to these gating checks — they must explicitly call `registerInterest` for each event type they want the runtime to count as having a consumer. Multiple registrations for the same event type from the same or different consumers are tracked independently and must each be released. See: `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, `session_limits_exhausted.requested`, `user_input.requested`, `elicitation.requested`, `command.queued`, `exit_plan_mode.requested`. /// The to monitor for cancellation requests. The default is . /// Opaque handle representing an event-type interest registration. public async Task RegisterInterestAsync(string eventType, CancellationToken cancellationToken = default) @@ -23182,8 +23794,12 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.AssistantReasoningDeltaData), TypeInfoPropertyName = "SessionEventsAssistantReasoningDeltaData")] [JsonSerializable(typeof(GitHub.Copilot.AssistantReasoningDeltaEvent), TypeInfoPropertyName = "SessionEventsAssistantReasoningDeltaEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantReasoningEvent), TypeInfoPropertyName = "SessionEventsAssistantReasoningEvent")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantServerToolProgressData), TypeInfoPropertyName = "SessionEventsAssistantServerToolProgressData")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantServerToolProgressEvent), TypeInfoPropertyName = "SessionEventsAssistantServerToolProgressEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantStreamingDeltaData), TypeInfoPropertyName = "SessionEventsAssistantStreamingDeltaData")] [JsonSerializable(typeof(GitHub.Copilot.AssistantStreamingDeltaEvent), TypeInfoPropertyName = "SessionEventsAssistantStreamingDeltaEvent")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantToolCallDeltaData), TypeInfoPropertyName = "SessionEventsAssistantToolCallDeltaData")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantToolCallDeltaEvent), TypeInfoPropertyName = "SessionEventsAssistantToolCallDeltaEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantTurnEndData), TypeInfoPropertyName = "SessionEventsAssistantTurnEndData")] [JsonSerializable(typeof(GitHub.Copilot.AssistantTurnEndEvent), TypeInfoPropertyName = "SessionEventsAssistantTurnEndEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantTurnStartData), TypeInfoPropertyName = "SessionEventsAssistantTurnStartData")] @@ -23217,6 +23833,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.AttachmentSelectionDetailsEnd), TypeInfoPropertyName = "SessionEventsAttachmentSelectionDetailsEnd")] [JsonSerializable(typeof(GitHub.Copilot.AttachmentSelectionDetailsStart), TypeInfoPropertyName = "SessionEventsAttachmentSelectionDetailsStart")] [JsonSerializable(typeof(GitHub.Copilot.AutoApprovalRecommendation), TypeInfoPropertyName = "SessionEventsAutoApprovalRecommendation")] +[JsonSerializable(typeof(GitHub.Copilot.AutoModeResolvedReasoningBucket), TypeInfoPropertyName = "SessionEventsAutoModeResolvedReasoningBucket")] [JsonSerializable(typeof(GitHub.Copilot.AutoModeSwitchCompletedData), TypeInfoPropertyName = "SessionEventsAutoModeSwitchCompletedData")] [JsonSerializable(typeof(GitHub.Copilot.AutoModeSwitchCompletedEvent), TypeInfoPropertyName = "SessionEventsAutoModeSwitchCompletedEvent")] [JsonSerializable(typeof(GitHub.Copilot.AutoModeSwitchRequestedData), TypeInfoPropertyName = "SessionEventsAutoModeSwitchRequestedData")] @@ -23279,6 +23896,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.GitHubRepoRef), TypeInfoPropertyName = "SessionEventsGitHubRepoRef")] [JsonSerializable(typeof(GitHub.Copilot.HandoffRepository), TypeInfoPropertyName = "SessionEventsHandoffRepository")] [JsonSerializable(typeof(GitHub.Copilot.HandoffSourceType), TypeInfoPropertyName = "SessionEventsHandoffSourceType")] +[JsonSerializable(typeof(GitHub.Copilot.HeaderEntry), TypeInfoPropertyName = "SessionEventsHeaderEntry")] [JsonSerializable(typeof(GitHub.Copilot.HookEndData), TypeInfoPropertyName = "SessionEventsHookEndData")] [JsonSerializable(typeof(GitHub.Copilot.HookEndError), TypeInfoPropertyName = "SessionEventsHookEndError")] [JsonSerializable(typeof(GitHub.Copilot.HookEndEvent), TypeInfoPropertyName = "SessionEventsHookEndEvent")] @@ -23286,6 +23904,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.HookProgressEvent), TypeInfoPropertyName = "SessionEventsHookProgressEvent")] [JsonSerializable(typeof(GitHub.Copilot.HookStartData), TypeInfoPropertyName = "SessionEventsHookStartData")] [JsonSerializable(typeof(GitHub.Copilot.HookStartEvent), TypeInfoPropertyName = "SessionEventsHookStartEvent")] +[JsonSerializable(typeof(GitHub.Copilot.ManagedSettingsResolvedSource), TypeInfoPropertyName = "SessionEventsManagedSettingsResolvedSource")] [JsonSerializable(typeof(GitHub.Copilot.McpAppToolCallCompleteData), TypeInfoPropertyName = "SessionEventsMcpAppToolCallCompleteData")] [JsonSerializable(typeof(GitHub.Copilot.McpAppToolCallCompleteError), TypeInfoPropertyName = "SessionEventsMcpAppToolCallCompleteError")] [JsonSerializable(typeof(GitHub.Copilot.McpAppToolCallCompleteEvent), TypeInfoPropertyName = "SessionEventsMcpAppToolCallCompleteEvent")] @@ -23300,15 +23919,19 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.McpOauthCompletedData), TypeInfoPropertyName = "SessionEventsMcpOauthCompletedData")] [JsonSerializable(typeof(GitHub.Copilot.McpOauthCompletedEvent), TypeInfoPropertyName = "SessionEventsMcpOauthCompletedEvent")] [JsonSerializable(typeof(GitHub.Copilot.McpOauthCompletionOutcome), TypeInfoPropertyName = "SessionEventsMcpOauthCompletionOutcome")] +[JsonSerializable(typeof(GitHub.Copilot.McpOauthHttpResponse), TypeInfoPropertyName = "SessionEventsMcpOauthHttpResponse")] [JsonSerializable(typeof(GitHub.Copilot.McpOauthRequestReason), TypeInfoPropertyName = "SessionEventsMcpOauthRequestReason")] [JsonSerializable(typeof(GitHub.Copilot.McpOauthRequiredData), TypeInfoPropertyName = "SessionEventsMcpOauthRequiredData")] [JsonSerializable(typeof(GitHub.Copilot.McpOauthRequiredEvent), TypeInfoPropertyName = "SessionEventsMcpOauthRequiredEvent")] [JsonSerializable(typeof(GitHub.Copilot.McpOauthRequiredStaticClientConfig), TypeInfoPropertyName = "SessionEventsMcpOauthRequiredStaticClientConfig")] [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.McpServerSource), TypeInfoPropertyName = "SessionEventsMcpServerSource")] [JsonSerializable(typeof(GitHub.Copilot.McpServerStatus), TypeInfoPropertyName = "SessionEventsMcpServerStatus")] [JsonSerializable(typeof(GitHub.Copilot.McpServerTransport), TypeInfoPropertyName = "SessionEventsMcpServerTransport")] [JsonSerializable(typeof(GitHub.Copilot.McpServersLoadedServer), TypeInfoPropertyName = "SessionEventsMcpServersLoadedServer")] +[JsonSerializable(typeof(GitHub.Copilot.McpToolsListChangedEvent), TypeInfoPropertyName = "SessionEventsMcpToolsListChangedEvent")] [JsonSerializable(typeof(GitHub.Copilot.ModelCallFailureBadRequestKind), TypeInfoPropertyName = "SessionEventsModelCallFailureBadRequestKind")] [JsonSerializable(typeof(GitHub.Copilot.ModelCallFailureData), TypeInfoPropertyName = "SessionEventsModelCallFailureData")] [JsonSerializable(typeof(GitHub.Copilot.ModelCallFailureEvent), TypeInfoPropertyName = "SessionEventsModelCallFailureEvent")] @@ -23466,6 +24089,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.UserToolSessionApprovalMemory), TypeInfoPropertyName = "SessionEventsUserToolSessionApprovalMemory")] [JsonSerializable(typeof(GitHub.Copilot.UserToolSessionApprovalRead), TypeInfoPropertyName = "SessionEventsUserToolSessionApprovalRead")] [JsonSerializable(typeof(GitHub.Copilot.UserToolSessionApprovalWrite), TypeInfoPropertyName = "SessionEventsUserToolSessionApprovalWrite")] +[JsonSerializable(typeof(GitHub.Copilot.Verbosity), TypeInfoPropertyName = "SessionEventsVerbosity")] [JsonSerializable(typeof(GitHub.Copilot.WorkingDirectoryContext), TypeInfoPropertyName = "SessionEventsWorkingDirectoryContext")] [JsonSerializable(typeof(GitHub.Copilot.WorkingDirectoryContextHostType), TypeInfoPropertyName = "SessionEventsWorkingDirectoryContextHostType")] [JsonSerializable(typeof(GitHub.Copilot.WorkspaceFileChangedOperation), TypeInfoPropertyName = "SessionEventsWorkspaceFileChangedOperation")] @@ -23661,6 +24285,17 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(McpRegisterExternalClientRequest))] [JsonSerializable(typeof(McpReloadWithConfigRequest))] [JsonSerializable(typeof(McpRemoveGitHubResult))] +[JsonSerializable(typeof(McpResource))] +[JsonSerializable(typeof(McpResourceAnnotations))] +[JsonSerializable(typeof(McpResourceContent))] +[JsonSerializable(typeof(McpResourceIcon))] +[JsonSerializable(typeof(McpResourceTemplate))] +[JsonSerializable(typeof(McpResourcesListRequest))] +[JsonSerializable(typeof(McpResourcesListResult))] +[JsonSerializable(typeof(McpResourcesListTemplatesRequest))] +[JsonSerializable(typeof(McpResourcesListTemplatesResult))] +[JsonSerializable(typeof(McpResourcesReadRequest))] +[JsonSerializable(typeof(McpResourcesReadResult))] [JsonSerializable(typeof(McpRestartServerRequest))] [JsonSerializable(typeof(McpSamplingExecutionResult))] [JsonSerializable(typeof(McpServer))] @@ -23672,6 +24307,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(McpStartServerRequest))] [JsonSerializable(typeof(McpStartServersResult))] [JsonSerializable(typeof(McpStopServerRequest))] +[JsonSerializable(typeof(McpToolUi))] [JsonSerializable(typeof(McpTools))] [JsonSerializable(typeof(McpUnregisterExternalClientRequest))] [JsonSerializable(typeof(MetadataContextAttributionResult))] @@ -23695,6 +24331,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ModeSetRequest))] [JsonSerializable(typeof(Model))] [JsonSerializable(typeof(ModelBilling))] +[JsonSerializable(typeof(ModelBillingPromo))] [JsonSerializable(typeof(ModelBillingTokenPrices))] [JsonSerializable(typeof(ModelBillingTokenPricesLongContext))] [JsonSerializable(typeof(ModelCapabilities))] @@ -23852,6 +24489,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SecretsAddFilterValuesRequest))] [JsonSerializable(typeof(SecretsAddFilterValuesResult))] [JsonSerializable(typeof(SendAttachmentsToMessageParams))] +[JsonSerializable(typeof(SendMessageItem))] +[JsonSerializable(typeof(SendMessagesRequest))] +[JsonSerializable(typeof(SendMessagesResult))] [JsonSerializable(typeof(SendRequest))] [JsonSerializable(typeof(SendResult))] [JsonSerializable(typeof(ServerAgentList))] @@ -23921,6 +24561,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionModeGetRequest))] [JsonSerializable(typeof(SessionModelGetCurrentRequest))] [JsonSerializable(typeof(SessionModelList))] +[JsonSerializable(typeof(SessionModelPriceCategory))] [JsonSerializable(typeof(SessionNameGetRequest))] [JsonSerializable(typeof(SessionOpenResult))] [JsonSerializable(typeof(SessionPlanDeleteRequest))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 26d63cfd61..41b87d06ff 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -32,7 +32,9 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(AssistantMessageStartEvent), "assistant.message_start")] [JsonDerivedType(typeof(AssistantReasoningEvent), "assistant.reasoning")] [JsonDerivedType(typeof(AssistantReasoningDeltaEvent), "assistant.reasoning_delta")] +[JsonDerivedType(typeof(AssistantServerToolProgressEvent), "assistant.server_tool_progress")] [JsonDerivedType(typeof(AssistantStreamingDeltaEvent), "assistant.streaming_delta")] +[JsonDerivedType(typeof(AssistantToolCallDeltaEvent), "assistant.tool_call_delta")] [JsonDerivedType(typeof(AssistantTurnEndEvent), "assistant.turn_end")] [JsonDerivedType(typeof(AssistantTurnStartEvent), "assistant.turn_start")] [JsonDerivedType(typeof(AssistantUsageEvent), "assistant.usage")] @@ -57,6 +59,9 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(McpHeadersRefreshRequiredEvent), "mcp.headers_refresh_required")] [JsonDerivedType(typeof(McpOauthCompletedEvent), "mcp.oauth_completed")] [JsonDerivedType(typeof(McpOauthRequiredEvent), "mcp.oauth_required")] +[JsonDerivedType(typeof(McpPromptsListChangedEvent), "mcp.prompts.list_changed")] +[JsonDerivedType(typeof(McpResourcesListChangedEvent), "mcp.resources.list_changed")] +[JsonDerivedType(typeof(McpToolsListChangedEvent), "mcp.tools.list_changed")] [JsonDerivedType(typeof(ModelCallFailureEvent), "model.call_failure")] [JsonDerivedType(typeof(PendingMessagesModifiedEvent), "pending_messages.modified")] [JsonDerivedType(typeof(PermissionCompletedEvent), "permission.completed")] @@ -65,6 +70,7 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(SamplingRequestedEvent), "sampling.requested")] [JsonDerivedType(typeof(SessionLimitsExhaustedCompletedEvent), "session_limits_exhausted.completed")] [JsonDerivedType(typeof(SessionLimitsExhaustedRequestedEvent), "session_limits_exhausted.requested")] +[JsonDerivedType(typeof(SessionAutoModeResolvedEvent), "session.auto_mode_resolved")] [JsonDerivedType(typeof(SessionAutopilotObjectiveChangedEvent), "session.autopilot_objective_changed")] [JsonDerivedType(typeof(SessionBackgroundTasksChangedEvent), "session.background_tasks_changed")] [JsonDerivedType(typeof(SessionBinaryAssetEvent), "session.binary_asset")] @@ -85,6 +91,7 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(SessionHandoffEvent), "session.handoff")] [JsonDerivedType(typeof(SessionIdleEvent), "session.idle")] [JsonDerivedType(typeof(SessionInfoEvent), "session.info")] +[JsonDerivedType(typeof(SessionManagedSettingsResolvedEvent), "session.managed_settings_resolved")] [JsonDerivedType(typeof(SessionMcpServerStatusChangedEvent), "session.mcp_server_status_changed")] [JsonDerivedType(typeof(SessionMcpServersLoadedEvent), "session.mcp_servers_loaded")] [JsonDerivedType(typeof(SessionModeChangedEvent), "session.mode_changed")] @@ -597,6 +604,19 @@ public sealed partial class AssistantIntentEvent : SessionEvent public required AssistantIntentData Data { get; set; } } +/// Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message. +/// Represents the assistant.server_tool_progress event. +public sealed partial class AssistantServerToolProgressEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "assistant.server_tool_progress"; + + /// The assistant.server_tool_progress event payload. + [JsonPropertyName("data")] + public required AssistantServerToolProgressData Data { get; set; } +} + /// Assistant reasoning content for timeline display with complete thinking text. /// Represents the assistant.reasoning event. public sealed partial class AssistantReasoningEvent : SessionEvent @@ -623,6 +643,19 @@ public sealed partial class AssistantReasoningDeltaEvent : SessionEvent public required AssistantReasoningDeltaData Data { get; set; } } +/// Streaming tool-call input delta for incremental tool-call updates. +/// Represents the assistant.tool_call_delta event. +public sealed partial class AssistantToolCallDeltaEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "assistant.tool_call_delta"; + + /// The assistant.tool_call_delta event payload. + [JsonPropertyName("data")] + public required AssistantToolCallDeltaData Data { get; set; } +} + /// Streaming response progress with cumulative byte count. /// Represents the assistant.streaming_delta event. public sealed partial class AssistantStreamingDeltaEvent : SessionEvent @@ -1248,6 +1281,34 @@ public sealed partial class SessionLimitsExhaustedCompletedEvent : SessionEvent public required SessionLimitsExhaustedCompletedData Data { get; set; } } +/// Auto Intent resolution: the concrete model the session settled on for the first prompt of an auto-mode session, and why. Lets SDK clients render the chosen model and the full reason it was picked. The core selection fields (chosenModel/reasoningBucket/categoryScores) are stable; the routing-analytics fields (predictedLabel/confidence/candidateModels) mirror the upstream intent service and may evolve, hence the event's experimental stability. +/// Represents the session.auto_mode_resolved event. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionAutoModeResolvedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.auto_mode_resolved"; + + /// The session.auto_mode_resolved event payload. + [JsonPropertyName("data")] + public required SessionAutoModeResolvedData Data { get; set; } +} + +/// Enterprise managed-settings resolution: the effective managed settings the session applied and where they came from, so SDK clients can show users what is enterprise-managed and by which authority. 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; for a session-independent pull, use the SDK `getManagedSettings()` API, which returns the identical payload. Managed settings have a single authoritative source, so the highest-authority present layer (server > device) wins wholesale; `bypassPermissionsDisabled` is deny-wins across layers. Marked experimental while the managed-settings surface stabilizes. +/// Represents the session.managed_settings_resolved event. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionManagedSettingsResolvedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.managed_settings_resolved"; + + /// The session.managed_settings_resolved event payload. + [JsonPropertyName("data")] + public required SessionManagedSettingsResolvedData Data { get; set; } +} + /// SDK command registration change notification. /// Represents the commands.changed event. public sealed partial class CommandsChangedEvent : SessionEvent @@ -1378,6 +1439,45 @@ public sealed partial class SessionMcpServerStatusChangedEvent : SessionEvent public required SessionMcpServerStatusChangedData 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 +{ + /// + [JsonIgnore] + public override string Type => "mcp.tools.list_changed"; + + /// The mcp.tools.list_changed event payload. + [JsonPropertyName("data")] + public required McpToolsListChangedData Data { get; set; } +} + +/// Payload identifying the MCP server associated with a list change. +/// Represents the mcp.resources.list_changed event. +public sealed partial class McpResourcesListChangedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "mcp.resources.list_changed"; + + /// The mcp.resources.list_changed event payload. + [JsonPropertyName("data")] + public required McpResourcesListChangedData Data { get; set; } +} + +/// Payload identifying the MCP server associated with a list change. +/// Represents the mcp.prompts.list_changed event. +public sealed partial class McpPromptsListChangedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "mcp.prompts.list_changed"; + + /// The mcp.prompts.list_changed event payload. + [JsonPropertyName("data")] + public required McpPromptsListChangedData Data { get; set; } +} + /// Payload of `session.extensions_loaded` listing discovered extensions and their statuses. /// Represents the session.extensions_loaded event. public sealed partial class SessionExtensionsLoadedEvent : SessionEvent @@ -1391,7 +1491,7 @@ public sealed partial class SessionExtensionsLoadedEvent : SessionEvent public required SessionExtensionsLoadedData Data { get; set; } } -/// Payload of `session.canvas.opened` with canvas instance and provider IDs plus optional title, status, URL, and input. +/// Payload of `session.canvas.opened` with canvas instance and provider IDs plus optional icon, title, status, URL, and input. /// Represents the session.canvas.opened event. [Experimental(Diagnostics.Experimental)] public sealed partial class SessionCanvasOpenedEvent : SessionEvent @@ -1565,6 +1665,11 @@ public sealed partial class SessionStartData [JsonPropertyName("startTime")] public required DateTimeOffset StartTime { get; set; } + /// Output verbosity level used for model calls, if applicable (e.g. "low", "medium", "high"). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("verbosity")] + public Verbosity? Verbosity { get; set; } + /// Schema version number for the session event format. [JsonPropertyName("version")] public required long Version { get; set; } @@ -1635,6 +1740,11 @@ public sealed partial class SessionResumeData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("sessionWasActive")] public bool? SessionWasActive { get; set; } + + /// Output verbosity level used for model calls, if applicable (e.g. "low", "medium", "high"). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("verbosity")] + public Verbosity? Verbosity { get; set; } } /// Notifies that the session's remote steering capability has changed. @@ -1866,6 +1976,11 @@ public sealed partial class SessionModelChangeData [JsonPropertyName("previousReasoningSummary")] public ReasoningSummary? PreviousReasoningSummary { get; set; } + /// Output verbosity level before the model change, if applicable. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("previousVerbosity")] + public Verbosity? PreviousVerbosity { get; set; } + /// Reasoning effort level after the model change, if applicable. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningEffort")] @@ -1875,6 +1990,11 @@ public sealed partial class SessionModelChangeData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningSummary")] public ReasoningSummary? ReasoningSummary { get; set; } + + /// Output verbosity level after the model change, if applicable. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("verbosity")] + public Verbosity? Verbosity { get; set; } } /// Agent mode change details including previous and new modes. @@ -2421,6 +2541,22 @@ public sealed partial class AssistantIntentData public required string Intent { get; set; } } +/// Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message. +public sealed partial class AssistantServerToolProgressData +{ + /// Kind of hosted server tool that is running. Only `web_search` is emitted today. + [JsonPropertyName("kind")] + public required string Kind { get; set; } + + /// Position of the hosted tool call in the response output. Stable across the call's lifecycle events (unlike the provider's per-event item id, which CAPI rotates), so the host keys the live in-progress row on it. + [JsonPropertyName("outputIndex")] + public required long OutputIndex { get; set; } + + /// Lifecycle status of the hosted call: `in_progress`, `searching`, or `completed`. + [JsonPropertyName("status")] + public required string Status { get; set; } +} + /// Assistant reasoning content for timeline display with complete thinking text. public sealed partial class AssistantReasoningData { @@ -2445,6 +2581,28 @@ public sealed partial class AssistantReasoningDeltaData public required string ReasoningId { get; set; } } +/// Streaming tool-call input delta for incremental tool-call updates. +public sealed partial class AssistantToolCallDeltaData +{ + /// Raw provider tool input fragment to append for this tool call. Function/tool-use providers stream serialized JSON argument text (so newlines inside JSON string values may appear as escaped `\n` until the accumulated JSON is parsed); custom tool calls stream raw custom input. + [JsonPropertyName("inputDelta")] + public required string InputDelta { get; set; } + + /// Tool call ID this delta belongs to, matching the corresponding assistant.message tool request. + [JsonPropertyName("toolCallId")] + public required string ToolCallId { get; set; } + + /// Name of the tool being invoked, when known from the stream. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } + + /// Tool call type, when known from the stream. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolType")] + public AssistantMessageToolRequestType? ToolType { get; set; } +} + /// Streaming response progress with cumulative byte count. public sealed partial class AssistantStreamingDeltaData { @@ -2467,6 +2625,11 @@ public sealed partial class AssistantMessageData [JsonPropertyName("citations")] public Citations? Citations { get; set; } + /// Client-minted request id (x-request-id header) echoed by the server. Distinct from requestId (x-github-request-id) and serviceRequestId (x-copilot-service-request-id). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("clientRequestId")] + public string? ClientRequestId { get; set; } + /// The assistant's text response content. [JsonPropertyName("content")] public required string Content { get; set; } @@ -2926,6 +3089,12 @@ public sealed partial class ToolExecutionCompleteData [JsonPropertyName("isUserRequested")] public bool? IsUserRequested { get; set; } + /// FIDES IFC label projected from tool ingress metadata (MCP `CallToolResult._meta` or synthesized built-in ingress labels). Persisted as `{ ifc: ... }` so the label survives session resume, including model-visible failure results. Experimental. + [Experimental(Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("mcpMeta")] + public JsonElement? McpMeta { get; set; } + /// Model identifier that generated this tool call. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("model")] @@ -3440,6 +3609,11 @@ public sealed partial class SamplingCompletedData /// OAuth authentication request for an MCP server. public sealed partial class McpOauthRequiredData { + /// Raw HTTP response details from the OAuth auth challenge, as observed by the runtime. Header order and casing are transport-dependent, and duplicate header names may appear multiple times. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("httpResponse")] + public McpOauthHttpResponse? HttpResponse { get; set; } + /// Why the runtime is requesting host-provided OAuth credentials. [JsonPropertyName("reason")] public required McpOauthRequestReason Reason { get; set; } @@ -3692,6 +3866,74 @@ public sealed partial class SessionLimitsExhaustedCompletedData public required SessionLimitsExhaustedResponse Response { get; set; } } +/// Auto Intent resolution: the concrete model the session settled on for the first prompt of an auto-mode session, and why. Lets SDK clients render the chosen model and the full reason it was picked. The core selection fields (chosenModel/reasoningBucket/categoryScores) are stable; the routing-analytics fields (predictedLabel/confidence/candidateModels) mirror the upstream intent service and may evolve, hence the event's experimental stability. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionAutoModeResolvedData +{ + /// Ordered candidate model list the router returned, when not a fallback. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("candidateModels")] + public string[]? CandidateModels { get; set; } + + /// Per-category classifier scores (0-1) behind the bucket: the granular HYDRA capability scores (reasoning, code_gen, debugging, tool_use), or the binary needs_reasoning/no_reasoning scores when HYDRA didn't run. Lets clients show a breakdown rather than just the bucket. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("categoryScores")] + public IDictionary? CategoryScores { get; set; } + + /// The concrete model the session will use after any intent refinement. + [JsonPropertyName("chosenModel")] + public required string ChosenModel { get; set; } + + /// Classifier confidence for the predicted label, when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("confidence")] + public double? Confidence { get; set; } + + /// The predicted classifier label (e.g. `needs_reasoning`), when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("predictedLabel")] + public string? PredictedLabel { get; set; } + + /// Coarse request-difficulty bucket, for explaining why a model was chosen ("picked X because this looks like high-reasoning work"). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningBucket")] + public AutoModeResolvedReasoningBucket? ReasoningBucket { get; set; } +} + +/// Enterprise managed-settings resolution: the effective managed settings the session applied and where they came from, so SDK clients can show users what is enterprise-managed and by which authority. 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; for a session-independent pull, use the SDK `getManagedSettings()` API, which returns the identical payload. Managed settings have a single authoritative source, so the highest-authority present layer (server > device) wins wholesale; `bypassPermissionsDisabled` is deny-wins across layers. Marked experimental while the managed-settings surface stabilizes. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionManagedSettingsResolvedData +{ + /// Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true. + [JsonPropertyName("bypassPermissionsDisabled")] + public required bool BypassPermissionsDisabled { get; set; } + + /// Whether the device (MDM/plist/registry/file) managed-settings layer was present. + [JsonPropertyName("deviceManaged")] + public required bool DeviceManaged { get; set; } + + /// 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. + [JsonPropertyName("failClosed")] + public required bool FailClosed { get; set; } + + /// The setting keys under enterprise management in the effective managed settings (e.g. `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force. + [JsonPropertyName("managedKeys")] + public required string[] ManagedKeys { get; set; } + + /// Whether the server (account/org) managed-settings layer was present. + [JsonPropertyName("serverManaged")] + public required bool ServerManaged { get; set; } + + /// The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("settings")] + public JsonElement? Settings { get; set; } + + /// Which channel supplied the effective managed settings (the winning layer), or `none` when no policy is in force. + [JsonPropertyName("source")] + public required ManagedSettingsResolvedSource Source { get; set; } +} + /// SDK command registration change notification. public sealed partial class CommandsChangedData { @@ -3823,6 +4065,30 @@ public sealed partial class SessionMcpServerStatusChangedData public required McpServerStatus Status { get; set; } } +/// Payload identifying the MCP server associated with a list change. +public sealed partial class McpToolsListChangedData +{ + /// Name of the MCP server whose list changed. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} + +/// Payload identifying the MCP server associated with a list change. +public sealed partial class McpResourcesListChangedData +{ + /// Name of the MCP server whose list changed. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} + +/// Payload identifying the MCP server associated with a list change. +public sealed partial class McpPromptsListChangedData +{ + /// Name of the MCP server whose list changed. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} + /// Payload of `session.extensions_loaded` listing discovered extensions and their statuses. public sealed partial class SessionExtensionsLoadedData { @@ -3831,7 +4097,7 @@ public sealed partial class SessionExtensionsLoadedData public required ExtensionsLoadedExtension[] Extensions { get; set; } } -/// Payload of `session.canvas.opened` with canvas instance and provider IDs plus optional title, status, URL, and input. +/// Payload of `session.canvas.opened` with canvas instance and provider IDs plus optional icon, title, status, URL, and input. [Experimental(Diagnostics.Experimental)] public sealed partial class SessionCanvasOpenedData { @@ -3848,6 +4114,11 @@ public sealed partial class SessionCanvasOpenedData [JsonPropertyName("extensionName")] public string? ExtensionName { get; set; } + /// Host-local PNG path for the canvas icon, when supplied. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("icon")] + public string? Icon { get; set; } + /// Input supplied when the instance was opened. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("input")] @@ -5227,7 +5498,7 @@ public sealed partial class ToolExecutionStartToolDescription /// MCP Apps metadata for UI resource association. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("_meta")] - public ToolExecutionStartToolDescriptionMeta? _meta { get; set; } + public ToolExecutionStartToolDescriptionMeta? Meta { get; set; } /// Tool description. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -5913,7 +6184,7 @@ public sealed partial class ToolExecutionCompleteUIResource /// Resource-level UI metadata (CSP, permissions, visual preferences). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("_meta")] - public ToolExecutionCompleteUIResourceMeta? _meta { get; set; } + public ToolExecutionCompleteUIResourceMeta? Meta { get; set; } /// Base64-encoded HTML content. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -5964,6 +6235,12 @@ public sealed partial class ToolExecutionCompleteResult [JsonPropertyName("detailedContent")] public string? DetailedContent { get; set; } + /// FIDES IFC label projected from tool ingress metadata (MCP `CallToolResult._meta` or synthesized built-in ingress labels) — persisted as `{ ifc: ... }` (only the `ifc` key, not the whole `_meta`). Persisted so the FIDES IFC label survives session resume: the engine rehydrates accumulated taint by replaying these on load. Populated for ingress sources when FIDES IFC is on. Experimental. + [Experimental(Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("mcpMeta")] + public JsonElement? McpMeta { get; set; } + /// Structured content (arbitrary JSON) returned verbatim by the MCP tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("structuredContent")] @@ -6007,7 +6284,7 @@ public sealed partial class ToolExecutionCompleteToolDescription /// MCP Apps metadata for UI resource association. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("_meta")] - public ToolExecutionCompleteToolDescriptionMeta? _meta { get; set; } + public ToolExecutionCompleteToolDescriptionMeta? Meta { get; set; } /// Tool description. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -6325,6 +6602,16 @@ public sealed partial class PermissionRequestWrite : PermissionRequest [JsonPropertyName("newFileContents")] public string? NewFileContents { get; set; } + /// True when a built-in file tool (apply_patch / str_replace_editor) asked to write a path the sandbox filesystem policy would block, and the host opted in via sandbox.allowBypass. This is a request, not a grant: the write happens 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; } + + /// Justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("requestSandboxBypassReason")] + public string? RequestSandboxBypassReason { get; set; } + /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolCallId")] @@ -6410,6 +6697,16 @@ public sealed partial class PermissionRequestUrl : PermissionRequest [JsonPropertyName("intention")] public required string Intention { 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. + [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. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("requestSandboxBypassReason")] + public string? RequestSandboxBypassReason { get; set; } + /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolCallId")] @@ -6766,6 +7063,16 @@ public sealed partial class PermissionPromptRequestUrl : PermissionPromptRequest [JsonPropertyName("intention")] public required string Intention { 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. + [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. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("requestSandboxBypassReason")] + public string? RequestSandboxBypassReason { get; set; } + /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolCallId")] @@ -7302,6 +7609,37 @@ public sealed partial class ElicitationRequestedSchema public required string Type { get; set; } } +/// Single HTTP header entry as a name/value pair. +/// Nested data type for HeaderEntry. +public sealed partial class HeaderEntry +{ + /// HTTP response header name as observed by the runtime. + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// HTTP response header value as observed by the runtime. + [JsonPropertyName("value")] + public required string Value { get; set; } +} + +/// Raw HTTP response details from the OAuth auth challenge, as observed by the runtime. +/// Nested data type for McpOauthHttpResponse. +public sealed partial class McpOauthHttpResponse +{ + /// Complete UTF-8 response body for host-specific challenge handling, including an empty string for an empty body. Omitted when the complete body is not valid UTF-8; body read failures fail the HTTP operation rather than exposing a partial response. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("body")] + public string? Body { get; set; } + + /// HTTP response headers as observed by the runtime. Order and casing are transport-dependent, and duplicate header names may appear multiple times. + [JsonPropertyName("headers")] + public required HeaderEntry[] Headers { get; set; } + + /// HTTP status code returned with the auth challenge. + [JsonPropertyName("statusCode")] + public required int StatusCode { get; set; } +} + /// Static OAuth client configuration, if the server specifies one. /// Nested data type for McpOauthRequiredStaticClientConfig. public sealed partial class McpOauthRequiredStaticClientConfig @@ -7584,6 +7922,11 @@ public sealed partial class CanvasRegistryChangedCanvas [JsonPropertyName("extensionName")] public string? ExtensionName { get; set; } + /// Host-local PNG path for the canvas icon, when supplied. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("icon")] + public string? Icon { get; set; } + /// JSON Schema for canvas open input. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("inputSchema")] @@ -7810,6 +8153,70 @@ public override void Write(Utf8JsonWriter writer, ReasoningSummary value, JsonSe } } +/// Output verbosity level used for supported model calls (e.g. "low", "medium", "high"). +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct Verbosity : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public Verbosity(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A terse response was requested. + public static Verbosity Low { get; } = new("low"); + + /// A medium amount of response detail was requested. + public static Verbosity Medium { get; } = new("medium"); + + /// A more detailed response was requested. + public static Verbosity High { get; } = new("high"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(Verbosity left, Verbosity right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(Verbosity left, Verbosity right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is Verbosity other && Equals(other); + + /// + public bool Equals(Verbosity 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 Verbosity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, Verbosity value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(Verbosity)); + } + } +} + /// The type of operation performed on the autopilot objective state file. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -8573,46 +8980,42 @@ public override void Write(Utf8JsonWriter writer, UserMessageDelivery value, Jso } } -/// The system that produced a citation. -[Experimental(Diagnostics.Experimental)] +/// Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CitationProvider : IEquatable +public readonly struct AssistantMessageToolRequestType : 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 CitationProvider(string value) + public AssistantMessageToolRequestType(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; - /// Citation produced by an Anthropic (Claude) model response. - public static CitationProvider Anthropic { get; } = new("anthropic"); - - /// Citation produced by an OpenAI model response. - public static CitationProvider Openai { get; } = new("openai"); + /// Standard function-style tool call. + public static AssistantMessageToolRequestType Function { get; } = new("function"); - /// Citation synthesized client-side by the runtime from tool output. - public static CitationProvider Client { get; } = new("client"); + /// Custom grammar-based tool call. + public static AssistantMessageToolRequestType Custom { get; } = new("custom"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CitationProvider left, CitationProvider right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AssistantMessageToolRequestType left, AssistantMessageToolRequestType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CitationProvider left, CitationProvider right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AssistantMessageToolRequestType left, AssistantMessageToolRequestType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CitationProvider other && Equals(other); + public override bool Equals(object? obj) => obj is AssistantMessageToolRequestType other && Equals(other); /// - public bool Equals(CitationProvider other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AssistantMessageToolRequestType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -8620,60 +9023,64 @@ public CitationProvider(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 CitationProvider Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AssistantMessageToolRequestType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CitationProvider value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AssistantMessageToolRequestType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CitationProvider)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AssistantMessageToolRequestType)); } } } -/// Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. +/// The system that produced a citation. +[Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AssistantMessageToolRequestType : IEquatable +public readonly struct CitationProvider : 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 AssistantMessageToolRequestType(string value) + public CitationProvider(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; - /// Standard function-style tool call. - public static AssistantMessageToolRequestType Function { get; } = new("function"); + /// Citation produced by an Anthropic (Claude) model response. + public static CitationProvider Anthropic { get; } = new("anthropic"); - /// Custom grammar-based tool call. - public static AssistantMessageToolRequestType Custom { get; } = new("custom"); + /// Citation produced by an OpenAI model response. + public static CitationProvider Openai { get; } = new("openai"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AssistantMessageToolRequestType left, AssistantMessageToolRequestType right) => left.Equals(right); + /// Citation synthesized client-side by the runtime from tool output. + public static CitationProvider Client { get; } = new("client"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AssistantMessageToolRequestType left, AssistantMessageToolRequestType right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CitationProvider left, CitationProvider right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CitationProvider left, CitationProvider right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AssistantMessageToolRequestType other && Equals(other); + public override bool Equals(object? obj) => obj is CitationProvider other && Equals(other); /// - public bool Equals(AssistantMessageToolRequestType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CitationProvider other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -8681,20 +9088,20 @@ public AssistantMessageToolRequestType(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 AssistantMessageToolRequestType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CitationProvider Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AssistantMessageToolRequestType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CitationProvider value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AssistantMessageToolRequestType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CitationProvider)); } } } @@ -10334,6 +10741,134 @@ public override void Write(Utf8JsonWriter writer, SessionLimitsExhaustedResponse } } +/// Coarse request-difficulty bucket for UX explainability. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AutoModeResolvedReasoningBucket : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AutoModeResolvedReasoningBucket(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The request looks low-reasoning; a lighter model is appropriate. + public static AutoModeResolvedReasoningBucket Low { get; } = new("low"); + + /// The request needs a moderate amount of reasoning. + public static AutoModeResolvedReasoningBucket Medium { get; } = new("medium"); + + /// The request looks high-reasoning; a stronger model is appropriate. + public static AutoModeResolvedReasoningBucket High { get; } = new("high"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AutoModeResolvedReasoningBucket left, AutoModeResolvedReasoningBucket right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AutoModeResolvedReasoningBucket left, AutoModeResolvedReasoningBucket right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AutoModeResolvedReasoningBucket other && Equals(other); + + /// + public bool Equals(AutoModeResolvedReasoningBucket 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 AutoModeResolvedReasoningBucket Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AutoModeResolvedReasoningBucket value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AutoModeResolvedReasoningBucket)); + } + } +} + +/// Which channel supplied the effective enterprise managed settings (highest-authority present layer wins wholesale). +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ManagedSettingsResolvedSource : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ManagedSettingsResolvedSource(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Account/org policy self-fetched from the GitHub managed-settings endpoint (higher authority). + public static ManagedSettingsResolvedSource Server { get; } = new("server"); + + /// Device-level MDM policy discovered from plist/registry/file (lower authority). + public static ManagedSettingsResolvedSource Device { get; } = new("device"); + + /// No managed policy is in force (no layer contributed). + public static ManagedSettingsResolvedSource None { get; } = new("none"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ManagedSettingsResolvedSource left, ManagedSettingsResolvedSource right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ManagedSettingsResolvedSource left, ManagedSettingsResolvedSource right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ManagedSettingsResolvedSource other && Equals(other); + + /// + public bool Equals(ManagedSettingsResolvedSource 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 ManagedSettingsResolvedSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ManagedSettingsResolvedSource value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ManagedSettingsResolvedSource)); + } + } +} + /// Exit plan mode action. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -10841,8 +11376,12 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(AssistantReasoningDeltaData))] [JsonSerializable(typeof(AssistantReasoningDeltaEvent))] [JsonSerializable(typeof(AssistantReasoningEvent))] +[JsonSerializable(typeof(AssistantServerToolProgressData))] +[JsonSerializable(typeof(AssistantServerToolProgressEvent))] [JsonSerializable(typeof(AssistantStreamingDeltaData))] [JsonSerializable(typeof(AssistantStreamingDeltaEvent))] +[JsonSerializable(typeof(AssistantToolCallDeltaData))] +[JsonSerializable(typeof(AssistantToolCallDeltaEvent))] [JsonSerializable(typeof(AssistantTurnEndData))] [JsonSerializable(typeof(AssistantTurnEndEvent))] [JsonSerializable(typeof(AssistantTurnStartData))] @@ -10924,6 +11463,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(ExternalToolRequestedEvent))] [JsonSerializable(typeof(GitHubRepoRef))] [JsonSerializable(typeof(HandoffRepository))] +[JsonSerializable(typeof(HeaderEntry))] [JsonSerializable(typeof(HookEndData))] [JsonSerializable(typeof(HookEndError))] [JsonSerializable(typeof(HookEndEvent))] @@ -10942,11 +11482,18 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(McpHeadersRefreshRequiredEvent))] [JsonSerializable(typeof(McpOauthCompletedData))] [JsonSerializable(typeof(McpOauthCompletedEvent))] +[JsonSerializable(typeof(McpOauthHttpResponse))] [JsonSerializable(typeof(McpOauthRequiredData))] [JsonSerializable(typeof(McpOauthRequiredEvent))] [JsonSerializable(typeof(McpOauthRequiredStaticClientConfig))] [JsonSerializable(typeof(McpOauthWWWAuthenticateParams))] +[JsonSerializable(typeof(McpPromptsListChangedData))] +[JsonSerializable(typeof(McpPromptsListChangedEvent))] +[JsonSerializable(typeof(McpResourcesListChangedData))] +[JsonSerializable(typeof(McpResourcesListChangedEvent))] [JsonSerializable(typeof(McpServersLoadedServer))] +[JsonSerializable(typeof(McpToolsListChangedData))] +[JsonSerializable(typeof(McpToolsListChangedEvent))] [JsonSerializable(typeof(ModelCallFailureData))] [JsonSerializable(typeof(ModelCallFailureEvent))] [JsonSerializable(typeof(ModelCallFailureRequestFingerprint))] @@ -11000,6 +11547,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SamplingCompletedEvent))] [JsonSerializable(typeof(SamplingRequestedData))] [JsonSerializable(typeof(SamplingRequestedEvent))] +[JsonSerializable(typeof(SessionAutoModeResolvedData))] +[JsonSerializable(typeof(SessionAutoModeResolvedEvent))] [JsonSerializable(typeof(SessionAutopilotObjectiveChangedData))] [JsonSerializable(typeof(SessionAutopilotObjectiveChangedEvent))] [JsonSerializable(typeof(SessionBackgroundTasksChangedData))] @@ -11047,6 +11596,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SessionLimitsExhaustedRequestedData))] [JsonSerializable(typeof(SessionLimitsExhaustedRequestedEvent))] [JsonSerializable(typeof(SessionLimitsExhaustedResponse))] +[JsonSerializable(typeof(SessionManagedSettingsResolvedData))] +[JsonSerializable(typeof(SessionManagedSettingsResolvedEvent))] [JsonSerializable(typeof(SessionMcpServerStatusChangedData))] [JsonSerializable(typeof(SessionMcpServerStatusChangedEvent))] [JsonSerializable(typeof(SessionMcpServersLoadedData))] diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index f009faa0bd..04306b7f62 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -90,6 +90,13 @@ private sealed record EventSubscription(Type EventType, Action Han private readonly Channel _eventChannel = Channel.CreateUnbounded( new() { SingleReader = true }); + /// + /// Fixed name of the runtime's built-in tool-search tool. A client can + /// replace its behavior by registering a tool with this exact name and + /// OverridesBuiltInTool set to true. + /// + private const string ToolSearchToolName = "tool_search_tool"; + /// /// Gets the unique identifier for this session. /// @@ -841,6 +848,26 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, Arguments = arguments }; + // The built-in tool-search tool receives a snapshot of the session's + // currently initialized tools so an override can filter the live + // catalog without issuing its own RPC. Fetch it only for that tool + // to avoid a round-trip on every tool call; a failed fetch leaves + // the snapshot null rather than failing the tool. + if (toolName == ToolSearchToolName) + { + try + { + var metadata = await Rpc.Tools.GetCurrentMetadataAsync(); + invocation.AvailableTools = metadata.Tools; + } + catch (Exception ex) when (ex is RemoteRpcException or IOException or ObjectDisposedException or JsonException) + { + // A failed metadata fetch is non-fatal: leave AvailableTools + // null so the tool still runs without the snapshot. + LogToolMetadataFetchFailed(ex, toolName); + } + } + var aiFunctionArgs = new AIFunctionArguments { Context = new Dictionary @@ -1091,6 +1118,7 @@ private void UpdateOpenCanvasesFromEvent(SessionEvent sessionEvent) InstanceId = data.InstanceId, Status = data.Status, Title = data.Title, + Icon = data.Icon, Url = data.Url, }); } @@ -1748,15 +1776,15 @@ public async Task AbortAsync(CancellationToken cancellationToken = default) /// Changes the model for this session. /// The new model takes effect for the next message. Conversation history is preserved. /// - /// Model ID to switch to (e.g., "gpt-4.1"). + /// Model ID to switch to (e.g., "gpt-5.4"). /// Reasoning effort level (e.g., "low", "medium", "high", "xhigh"). /// Per-property overrides for model capabilities, deep-merged over runtime defaults. /// Optional cancellation token. /// /// - /// await session.SetModelAsync("gpt-4.1"); + /// await session.SetModelAsync("gpt-5.4"); /// await session.SetModelAsync("claude-sonnet-4.6", "high"); - /// await session.SetModelAsync("gpt-4.1", new SetModelOptions { ContextTier = ContextTier.LongContext }); + /// await session.SetModelAsync("gpt-5.4", new SetModelOptions { ContextTier = ContextTier.LongContext }); /// /// public Task SetModelAsync(string model, string? reasoningEffort, ModelCapabilitiesOverride? modelCapabilities = null, CancellationToken cancellationToken = default) @@ -1775,7 +1803,7 @@ public Task SetModelAsync(string model, string? reasoningEffort, ModelCapabiliti /// Changes the model for this session. /// The new model takes effect for the next message. Conversation history is preserved. /// - /// Model ID to switch to (e.g., "gpt-4.1"). + /// Model ID to switch to (e.g., "gpt-5.4"). /// Settings for the new model. /// Optional cancellation token. public async Task SetModelAsync(string model, SetModelOptions options, CancellationToken cancellationToken = default) @@ -1787,6 +1815,7 @@ await Rpc.Model.SwitchToAsync( model, options.ReasoningEffort, options.ReasoningSummary, + null, options.ModelCapabilities, options.ContextTier, cancellationToken); @@ -1906,6 +1935,9 @@ await InvokeRpcAsync( [LoggerMessage(Level = LogLevel.Error, Message = "Unhandled exception in session event handler")] private partial void LogEventHandlerError(Exception exception); + [LoggerMessage(Level = LogLevel.Debug, Message = "Failed to fetch tool metadata for {toolName}")] + private partial void LogToolMetadataFetchFailed(Exception exception, string toolName); + internal record SendMessageRequest { public string SessionId { get; init; } = string.Empty; diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 65295d3d24..2bb643c75b 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -697,6 +697,12 @@ public sealed class ToolResultObject [JsonPropertyName("toolTelemetry")] public IDictionary? ToolTelemetry { get; set; } + /// + /// Names of tools returned by a tool-search tool. + /// + [JsonPropertyName("toolReferences")] + public IList? ToolReferences { get; set; } + /// /// Converts the result of an invocation into a /// . Handles , @@ -808,6 +814,14 @@ public sealed class ToolInvocation /// Arguments passed to the tool by the language model. /// public JsonElement? Arguments { get; set; } + /// + /// Snapshot of the session's currently initialized tools. The SDK populates + /// this only when the invocation targets the built-in tool-search tool + /// (tool_search_tool), so a tool-search override can rank/filter the + /// live catalog — including MCP tools configured in settings — without + /// issuing its own RPC. null for every other tool invocation. + /// + public IList? AvailableTools { get; set; } } /// @@ -2643,6 +2657,14 @@ public sealed class CustomAgentConfig /// [JsonPropertyName("model")] public string? Model { get; set; } + + /// + /// Reasoning effort level for this agent's model. + /// When omitted, no per-agent override is sent and the backend chooses its + /// default. The parent session effort is not inherited. + /// + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } } /// @@ -2721,6 +2743,30 @@ public sealed class LargeToolOutputConfig public string? OutputDirectory { get; set; } } +/// +/// Overrides the runtime's built-in tool-search behavior. +/// Defers tools to keep the model's active tool set small. +/// To override the tool-search tool's implementation, register a tool +/// named "tool_search_tool" with OverridesBuiltInTool set to +/// . +/// +public sealed class ToolSearchConfig +{ + /// + /// Enable or disable tool search. + /// + [JsonPropertyName("enabled")] + public bool? Enabled { get; set; } + + /// + /// The tool count above which MCP and external tools are deferred behind + /// tool search. When , the runtime default (30) + /// applies. + /// + [JsonPropertyName("deferThreshold")] + public int? DeferThreshold { get; set; } +} + /// /// Configuration for session memory. /// @@ -2829,6 +2875,7 @@ protected SessionConfigBase(SessionConfigBase? other) Hooks = other.Hooks; InfiniteSessions = other.InfiniteSessions; LargeOutput = other.LargeOutput; + ToolSearch = other.ToolSearch; Memory = other.Memory; McpServers = other.McpServers is not null ? (other.McpServers is Dictionary dict @@ -2861,12 +2908,14 @@ protected SessionConfigBase(SessionConfigBase? other) GitHubToken = other.GitHubToken; RemoteSession = other.RemoteSession; ExpAssignments = other.ExpAssignments; + EnableManagedSettings = other.EnableManagedSettings; #pragma warning disable GHCP001 Canvases = other.Canvases is not null ? [.. other.Canvases] : null; RequestCanvasRenderer = other.RequestCanvasRenderer; RequestExtensions = other.RequestExtensions; ExtensionSdkPath = other.ExtensionSdkPath; ExtensionInfo = other.ExtensionInfo; + CanvasProvider = other.CanvasProvider; CanvasHandler = other.CanvasHandler; #pragma warning restore GHCP001 SkillDirectories = other.SkillDirectories is not null ? [.. other.SkillDirectories] : null; @@ -3233,6 +3282,13 @@ protected SessionConfigBase(SessionConfigBase? other) /// public LargeToolOutputConfig? LargeOutput { get; set; } + /// + /// Overrides the runtime's built-in tool-search behavior. + /// Tool search defers tools to keep the model's active tool set small. When , + /// the runtime default applies. + /// + public ToolSearchConfig? ToolSearch { get; set; } + /// /// Configuration for session memory. When set, controls whether the /// session can read and write persistent memory. @@ -3285,6 +3341,16 @@ protected SessionConfigBase(SessionConfigBase? other) [EditorBrowsable(EditorBrowsableState.Never)] public JsonElement? ExpAssignments { get; set; } + /// + /// Opt-in: when true, the runtime self-fetches enterprise managed + /// settings (bypass-permissions policy) at session bootstrap using the + /// session's . Requires to + /// be set; if omitted, the runtime is expected to reject session creation + /// (fail-closed). When unset, behaves exactly as before. Serialized on the + /// wire as enableManagedSettings. + /// + public bool? EnableManagedSettings { get; set; } + #pragma warning disable GHCP001 /// /// Canvas declarations advertised by this connection. The runtime forwards @@ -3326,6 +3392,16 @@ protected SessionConfigBase(SessionConfigBase? other) [Experimental(Diagnostics.Experimental)] public ExtensionInfo? ExtensionInfo { get; set; } + /// + /// Stable identity for a host/SDK connection that supplies built-in + /// canvases. When set, the runtime uses + /// verbatim as the agent-facing canvas extension id, so canvases declared on + /// a control connection survive reconnect and CLI restart. Honored on + /// session create and resume. + /// + [Experimental(Diagnostics.Experimental)] + public CanvasProviderIdentity? CanvasProvider { get; set; } + /// /// Provider-side canvas lifecycle handler. The SDK routes inbound /// canvas.open / canvas.close / canvas.action.invoke @@ -4010,5 +4086,6 @@ public sealed class SystemMessageTransformRpcResponse [JsonSerializable(typeof(CanvasProviderOpenResult))] [JsonSerializable(typeof(CanvasHostContext))] [JsonSerializable(typeof(ExtensionInfo))] +[JsonSerializable(typeof(CanvasProviderIdentity))] #pragma warning restore GHCP001 internal partial class TypesJsonContext : JsonSerializerContext; diff --git a/dotnet/src/UnixMillisecondsDateTimeOffsetConverter.cs b/dotnet/src/UnixMillisecondsDateTimeOffsetConverter.cs index 4b8fcc3616..8e176fbafa 100644 --- a/dotnet/src/UnixMillisecondsDateTimeOffsetConverter.cs +++ b/dotnet/src/UnixMillisecondsDateTimeOffsetConverter.cs @@ -13,8 +13,14 @@ namespace GitHub.Copilot; public sealed class UnixMillisecondsDateTimeOffsetConverter : JsonConverter { /// - public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => - DateTimeOffset.FromUnixTimeMilliseconds(reader.GetInt64()); + public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // The CLI may serialize the epoch-millisecond timestamp as a JSON integer + // or as a floating-point number (e.g. 1700000000000.0). GetInt64 throws on a + // fractional token, so fall back to reading a double and truncating. + long milliseconds = reader.TryGetInt64(out long value) ? value : (long)reader.GetDouble(); + return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds); + } /// public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) => diff --git a/dotnet/src/build/GitHub.Copilot.SDK.targets b/dotnet/src/build/GitHub.Copilot.SDK.targets index 5a5e511811..5f7944b2c4 100644 --- a/dotnet/src/build/GitHub.Copilot.SDK.targets +++ b/dotnet/src/build/GitHub.Copilot.SDK.targets @@ -9,6 +9,7 @@ <_CopilotOs Condition="'$(RuntimeIdentifier)' != '' And $(RuntimeIdentifier.StartsWith('win'))">win <_CopilotOs Condition="'$(_CopilotOs)' == '' And '$(RuntimeIdentifier)' != '' And $(RuntimeIdentifier.StartsWith('osx'))">osx <_CopilotOs Condition="'$(_CopilotOs)' == '' And '$(RuntimeIdentifier)' != '' And $(RuntimeIdentifier.StartsWith('maccatalyst'))">osx + <_CopilotOs Condition="'$(_CopilotOs)' == '' And '$(RuntimeIdentifier)' != '' And $(RuntimeIdentifier.StartsWith('linux-musl'))">linux-musl <_CopilotOs Condition="'$(_CopilotOs)' == '' And '$(RuntimeIdentifier)' != ''">linux @@ -22,7 +23,7 @@ - + @@ -31,6 +32,8 @@ <_CopilotPlatform Condition="'$(_CopilotRid)' == 'win-arm64'">win32-arm64 <_CopilotPlatform Condition="'$(_CopilotRid)' == 'linux-x64'">linux-x64 <_CopilotPlatform Condition="'$(_CopilotRid)' == 'linux-arm64'">linux-arm64 + <_CopilotPlatform Condition="'$(_CopilotRid)' == 'linux-musl-x64'">linuxmusl-x64 + <_CopilotPlatform Condition="'$(_CopilotRid)' == 'linux-musl-arm64'">linuxmusl-arm64 <_CopilotPlatform Condition="'$(_CopilotRid)' == 'osx-x64'">darwin-x64 <_CopilotPlatform Condition="'$(_CopilotRid)' == 'osx-arm64'">darwin-arm64 <_CopilotBinary Condition="$(_CopilotRid.StartsWith('win-'))">copilot.exe diff --git a/dotnet/test/AssemblyInfo.cs b/dotnet/test/AssemblyInfo.cs index 9380ca48cd..6f5f258a91 100644 --- a/dotnet/test/AssemblyInfo.cs +++ b/dotnet/test/AssemblyInfo.cs @@ -5,7 +5,7 @@ using Xunit; using GitHub.Copilot.Test.Harness; -// Each E2E test class fixture spins up its own Copilot CLI subprocess plus a CapiProxy +// Each E2E test class fixture spins up its own Copilot CLI subprocess plus a ReplayProxy // (replaying HTTP proxy) Node.js subprocess. With ~25 test classes, running them in parallel // would launch ~50 long-lived Node.js processes simultaneously and exhaust both file // descriptors and memory on developer machines and CI runners (especially Windows). Tests diff --git a/dotnet/test/E2E/ByokBearerTokenProviderE2ETests.cs b/dotnet/test/E2E/ByokBearerTokenProviderE2ETests.cs index 5973dc61c6..4d2cb5e34d 100644 --- a/dotnet/test/E2E/ByokBearerTokenProviderE2ETests.cs +++ b/dotnet/test/E2E/ByokBearerTokenProviderE2ETests.cs @@ -36,6 +36,7 @@ namespace GitHub.Copilot.Test.E2E; /// and the resulting token reaches that provider's endpoint. /// /// +[Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public class ByokBearerTokenProviderE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "byok_bearer_token_provider", output) { diff --git a/dotnet/test/E2E/ClientE2ETests.cs b/dotnet/test/E2E/ClientE2ETests.cs index 5de6ce159a..d166223d65 100644 --- a/dotnet/test/E2E/ClientE2ETests.cs +++ b/dotnet/test/E2E/ClientE2ETests.cs @@ -9,8 +9,10 @@ namespace GitHub.Copilot.Test.E2E; // These tests bypass E2ETestBase because they are about how the CLI subprocess is started // Other test classes should instead inherit from E2ETestBase -public class ClientE2ETests +public class ClientE2ETests(E2ETestFixture fixture) : IClassFixture { + private E2ETestContext Ctx => fixture.Ctx; + [Theory] [InlineData(true)] // stdio transport [InlineData(false)] // TCP transport @@ -66,7 +68,7 @@ public async Task Should_Force_Stop_Without_Cleanup(bool useStdio) { using var client = new CopilotClient(new CopilotClientOptions { Connection = useStdio ? RuntimeConnection.ForStdio() : RuntimeConnection.ForTcp() }); - await client.CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll }); + await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll }); await client.ForceStopAsync(); } @@ -165,7 +167,7 @@ public async Task Should_List_Models_When_Authenticated(bool useStdio) public async Task Should_Not_Throw_When_Disposing_Session_After_Stopping_Client(bool useStdio) { await using var client = new CopilotClient(new CopilotClientOptions { Connection = useStdio ? RuntimeConnection.ForStdio() : RuntimeConnection.ForTcp() }); - await using var session = await client.CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll }); + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll }); await client.StopAsync(); } @@ -201,7 +203,7 @@ public async Task Should_Report_Error_With_Stderr_When_CLI_Fails_To_Start(bool u // Verify subsequent calls also fail (don't hang) var ex2 = await Assert.ThrowsAnyAsync(async () => { - var session = await client.CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll }); + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll }); await session.SendAsync(new MessageOptions { Prompt = "test" }); }); Assert.True( @@ -219,7 +221,7 @@ public async Task Should_Report_Error_With_Stderr_When_CLI_Fails_To_Start(bool u public async Task Should_Allow_CreateSession_Called_Without_PermissionHandler(bool useStdio) { await using var client = new CopilotClient(new CopilotClientOptions { Connection = useStdio ? RuntimeConnection.ForStdio() : RuntimeConnection.ForTcp() }); - await using var session = await client.CreateSessionAsync(new SessionConfig()); + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig()); Assert.NotNull(session.SessionId); } @@ -234,7 +236,7 @@ public async Task Should_Allow_ResumeSession_Called_Without_PermissionHandler() { Connection = RuntimeConnection.ForTcp(connectionToken: connectionToken), }); - await using var originalSession = await client.CreateSessionAsync(new SessionConfig()); + await using var originalSession = await ctx.CreateSessionAsync(client, new SessionConfig()); var port = client.RuntimePort ?? throw new InvalidOperationException("Client must be using TCP transport to support multi-client resume."); @@ -243,7 +245,7 @@ public async Task Should_Allow_ResumeSession_Called_Without_PermissionHandler() { Connection = RuntimeConnection.ForUri($"localhost:{port}", connectionToken: connectionToken), }); - await using var resumedSession = await resumeClient.ResumeSessionAsync(originalSession.SessionId, new()); + await using var resumedSession = await ctx.ResumeSessionAsync(resumeClient, originalSession.SessionId, new()); Assert.Equal(originalSession.SessionId, resumedSession.SessionId); } diff --git a/dotnet/test/E2E/ClientOptionsE2ETests.cs b/dotnet/test/E2E/ClientOptionsE2ETests.cs index a8ceda5b08..bf995563c4 100644 --- a/dotnet/test/E2E/ClientOptionsE2ETests.cs +++ b/dotnet/test/E2E/ClientOptionsE2ETests.cs @@ -7,6 +7,7 @@ using System.Net; using System.Net.Sockets; using System.Text.Json; +using GitHub.Copilot.Test.Harness; using Xunit; using Xunit.Abstractions; @@ -41,7 +42,7 @@ public async Task Should_Use_Client_Cwd_For_Default_WorkingDirectory() WorkingDirectory = clientCwd, }); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -110,7 +111,7 @@ public async Task Should_Propagate_Process_Options_To_Spawned_Cli() Assert.Equal("dotnet-sdk-e2e", capturedEnv.GetProperty("COPILOT_OTEL_SOURCE_NAME").GetString()); Assert.Equal("true", capturedEnv.GetProperty("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT").GetString()); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { EnableConfigDiscovery = true, IncludeSubAgentStreamingEvents = false, @@ -139,7 +140,7 @@ public async Task Should_Forward_EnableSessionTelemetry_In_Wire_Request() await client.StartAsync(); // When explicitly set to false, it should appear in the wire request - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { EnableSessionTelemetry = false, OnPermissionRequest = PermissionHandler.ApproveAll, @@ -166,7 +167,7 @@ public async Task Should_Omit_EnableSessionTelemetry_When_Not_Set() await client.StartAsync(); // When omitted (null/default), the field should not be present in the wire request - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -191,7 +192,7 @@ public async Task Should_Forward_Granular_Multitenancy_Fields_In_Create_Wire_Req await client.StartAsync(); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { SkipEmbeddingRetrieval = false, OrganizationCustomInstructions = "Follow org policy.", @@ -219,6 +220,7 @@ public async Task Should_Forward_Granular_Multitenancy_Fields_In_Create_Wire_Req } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request() { var (cliPath, capturePath) = await CreateFakeCliCaptureAsync(); @@ -232,7 +234,7 @@ public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request await client.StartAsync(); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { ClientName = "advanced-create-client", Model = "claude-sonnet-4.5", @@ -366,6 +368,7 @@ public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task Should_Forward_Singular_Provider_Options_In_Create_Wire_Request() { var (cliPath, capturePath) = await CreateFakeCliCaptureAsync(); @@ -378,7 +381,7 @@ public async Task Should_Forward_Singular_Provider_Options_In_Create_Wire_Reques await client.StartAsync(); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { Model = "claude-sonnet-4.5", Provider = new ProviderConfig @@ -432,7 +435,7 @@ public async Task Should_Apply_Empty_Mode_Defaults_To_CreateSession_Wire_Request await client.StartAsync(); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, AvailableTools = new ToolSet().AddBuiltIn(BuiltInTools.Isolated), @@ -471,7 +474,7 @@ public async Task Should_Propagate_Activity_TraceContext_To_Session_Create_And_S activity.TraceStateString = "vendor=create-send"; activity.Start(); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -555,7 +558,7 @@ public async Task Should_Propagate_Activity_TraceContext_To_Session_Resume() activity.TraceStateString = "vendor=resume"; activity.Start(); - var session = await client.ResumeSessionAsync("trace-resume-session", new ResumeSessionConfig + var session = await Ctx.ResumeSessionAsync(client, "trace-resume-session", new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -582,7 +585,7 @@ public async Task Should_Forward_Granular_Multitenancy_Fields_In_Resume_Wire_Req await client.StartAsync(); - var session = await client.ResumeSessionAsync("resume-session", new ResumeSessionConfig + var session = await Ctx.ResumeSessionAsync(client, "resume-session", new ResumeSessionConfig { SkipEmbeddingRetrieval = false, OrganizationCustomInstructions = "Resume org policy.", @@ -624,7 +627,7 @@ public async Task Should_Forward_Advanced_Session_Options_In_Resume_Wire_Request await client.StartAsync(); - var session = await client.ResumeSessionAsync("advanced-resume-session", new ResumeSessionConfig + var session = await Ctx.ResumeSessionAsync(client, "advanced-resume-session", new ResumeSessionConfig { ClientName = "advanced-resume-client", Model = "claude-haiku-4.5", @@ -706,7 +709,7 @@ public async Task Should_Apply_Empty_Mode_Defaults_To_ResumeSession_Wire_Request await client.StartAsync(); - var session = await client.ResumeSessionAsync("resume-empty-session", new ResumeSessionConfig + var session = await Ctx.ResumeSessionAsync(client, "resume-empty-session", new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, AvailableTools = new ToolSet().AddBuiltIn(BuiltInTools.Isolated), diff --git a/dotnet/test/E2E/CopilotRequestCancelErrorE2ETests.cs b/dotnet/test/E2E/CopilotRequestCancelErrorE2ETests.cs index 99e9e2546e..a9b645d2ac 100644 --- a/dotnet/test/E2E/CopilotRequestCancelErrorE2ETests.cs +++ b/dotnet/test/E2E/CopilotRequestCancelErrorE2ETests.cs @@ -54,7 +54,7 @@ public async Task Reports_A_Thrown_Callback_Error_Instead_Of_Hanging() await using var client = CreateClientWith(handler); await client.StartAsync(); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -81,7 +81,7 @@ public async Task Observes_Runtime_Cancellation_Of_An_In_Flight_Inference_Reques await using var client = CreateClientWith(handler); await client.StartAsync(); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); diff --git a/dotnet/test/E2E/CopilotRequestE2EProvider.cs b/dotnet/test/E2E/CopilotRequestE2EProvider.cs index bade5c6e5d..89826b4f84 100644 --- a/dotnet/test/E2E/CopilotRequestE2EProvider.cs +++ b/dotnet/test/E2E/CopilotRequestE2EProvider.cs @@ -56,7 +56,13 @@ protected override async Task SendRequestAsync(HttpRequestM #else : await request.Content.ReadAsStringAsync().ConfigureAwait(false); #endif - _records.Enqueue(new InterceptedRequest(url, ctx.SessionId, bodyText)); + _records.Enqueue(new InterceptedRequest( + url, + ctx.SessionId, + ctx.AgentId, + ctx.ParentAgentId, + ctx.InteractionType, + bodyText)); return IsInferenceUrl(url) ? BuildInferenceResponse(url, bodyText) @@ -188,4 +194,10 @@ internal static HttpResponseMessage BuildNonInferenceResponse(string url) } /// A single request the callback intercepted. -internal sealed record InterceptedRequest(string Url, string? SessionId, string Body); +internal sealed record InterceptedRequest( + string Url, + string? SessionId, + string? AgentId, + string? ParentAgentId, + string? InteractionType, + string Body); diff --git a/dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs b/dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs index e09c72c46e..fd00cc9b99 100644 --- a/dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs +++ b/dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs @@ -28,13 +28,14 @@ private CopilotClient CreateClientWith(RecordingRequestHandler provider) => }); [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] public async Task Threads_The_Session_Id_Into_A_Capi_Session_Inference_Request() { var provider = new RecordingRequestHandler(); await using var client = CreateClientWith(provider); await client.StartAsync(); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -53,20 +54,25 @@ public async Task Threads_The_Session_Id_Into_A_Capi_Session_Inference_Request() var inference = provider.InferenceRequests; Assert.NotEmpty(inference); - Assert.All(inference, r => Assert.Equal(capiSessionId, r.SessionId)); + Assert.All(inference, r => + { + Assert.Equal(capiSessionId, r.SessionId); + AssertAgentMetadata(r); + }); // Validate the final assistant response arrived (guards against truncated captures) Assert.Contains("OK from the synthetic", content); } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task Threads_The_Session_Id_Into_A_Byok_Session_Inference_Request() { var provider = new RecordingRequestHandler(); await using var client = CreateClientWith(provider); await client.StartAsync(); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, // BYOK providers require an explicit model id. @@ -96,9 +102,19 @@ public async Task Threads_The_Session_Id_Into_A_Byok_Session_Inference_Request() var inference = provider.InferenceRequests; Assert.NotEmpty(inference); - Assert.All(inference, r => Assert.Equal(byokSessionId, r.SessionId)); + Assert.All(inference, r => + { + Assert.Equal(byokSessionId, r.SessionId); + AssertAgentMetadata(r); + }); // Validate the final assistant response arrived (guards against truncated captures) Assert.Contains("OK from the synthetic", content); } + + private static void AssertAgentMetadata(InterceptedRequest request) + { + Assert.False(string.IsNullOrEmpty(request.AgentId)); + Assert.False(string.IsNullOrEmpty(request.InteractionType)); + } } diff --git a/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs b/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs index 9bb19df7d5..80ccdb8c90 100644 --- a/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs +++ b/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs @@ -31,6 +31,7 @@ namespace GitHub.Copilot.Test.E2E; /// message. Without the eager start the turn never completes and this test /// times out. /// +[Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public class CopilotRequestWebSocketE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "copilot_request_websocket", output) { @@ -54,7 +55,7 @@ public async Task Services_A_WebSocket_Turn_End_To_End_Via_The_Request_Handler() }, environment: env); await client.StartAsync(); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); diff --git a/dotnet/test/E2E/GitHubTelemetryForwardingE2ETests.cs b/dotnet/test/E2E/GitHubTelemetryForwardingE2ETests.cs index 343c4815b7..80d0ccb0b6 100644 --- a/dotnet/test/E2E/GitHubTelemetryForwardingE2ETests.cs +++ b/dotnet/test/E2E/GitHubTelemetryForwardingE2ETests.cs @@ -12,6 +12,9 @@ namespace GitHub.Copilot.Test.E2E; #pragma warning disable GHCP001 // GitHub telemetry forwarding is experimental. +// TODO(BYOK): Anthropic Messages produced no GitHub telemetry notification. Determine whether +// provider-backed sessions should forward the same telemetry before keeping this CAPI-only. +[Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] public class GitHubTelemetryForwardingE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "github_telemetry", output) { @@ -32,7 +35,7 @@ public async Task Should_Forward_GitHub_Telemetry_For_A_Live_Session() CopilotSession? session = null; try { - session = await client.CreateSessionAsync(new SessionConfig + session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); diff --git a/dotnet/test/E2E/ModeEmptyE2ETests.cs b/dotnet/test/E2E/ModeEmptyE2ETests.cs index df1bbc857d..433e6a745c 100644 --- a/dotnet/test/E2E/ModeEmptyE2ETests.cs +++ b/dotnet/test/E2E/ModeEmptyE2ETests.cs @@ -21,7 +21,7 @@ public class ModeEmptyE2ETests(E2ETestFixture fixture, ITestOutputHelper output) public async Task Empty_Mode_Isolated_Set_Shell_Tool_Is_Not_Exposed() { await using var client = Ctx.CreateClient(options: EmptyModeOptions(Ctx)); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, AvailableTools = new ToolSet().AddBuiltIn(BuiltInTools.Isolated), @@ -45,7 +45,7 @@ public async Task Empty_Mode_Isolated_Set_Shell_Tool_Is_Not_Exposed() public async Task Empty_Mode_Builtin_Star_Exposes_All_Built_In_Tools() { await using var client = Ctx.CreateClient(options: EmptyModeOptions(Ctx)); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, AvailableTools = new ToolSet().AddBuiltIn("*"), @@ -65,7 +65,7 @@ public async Task Empty_Mode_Excluded_Tools_Subtracts_From_Available_Tools() { var shellToolName = OperatingSystem.IsWindows() ? "powershell" : "bash"; await using var client = Ctx.CreateClient(options: EmptyModeOptions(Ctx)); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, AvailableTools = new ToolSet().AddBuiltIn("*"), @@ -85,7 +85,7 @@ public async Task Empty_Mode_Excluded_Tools_Subtracts_From_Available_Tools() public async Task Empty_Mode_Strips_Environment_Context_From_The_System_Message_By_Default() { await using var client = Ctx.CreateClient(options: EmptyModeOptions(Ctx)); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, AvailableTools = new ToolSet().AddBuiltIn(BuiltInTools.Isolated), @@ -109,7 +109,7 @@ public async Task Empty_Mode_Strips_Environment_Context_From_The_System_Message_ public async Task Empty_Mode_System_Message_Replace_Llm_Follows_Caller_Content_Verbatim() { await using var client = Ctx.CreateClient(options: EmptyModeOptions(Ctx)); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, AvailableTools = new ToolSet().AddBuiltIn(BuiltInTools.Isolated), @@ -128,7 +128,7 @@ public async Task Empty_Mode_System_Message_Replace_Llm_Follows_Caller_Content_V public async Task Empty_Mode_Append_Caller_Instruction_Takes_Effect_And_Env_Context_Stripped() { await using var client = Ctx.CreateClient(options: EmptyModeOptions(Ctx)); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, AvailableTools = new ToolSet().AddBuiltIn(BuiltInTools.Isolated), diff --git a/dotnet/test/E2E/ModeHandlersE2ETests.cs b/dotnet/test/E2E/ModeHandlersE2ETests.cs index a397999f73..b9f0e69b22 100644 --- a/dotnet/test/E2E/ModeHandlersE2ETests.cs +++ b/dotnet/test/E2E/ModeHandlersE2ETests.cs @@ -8,6 +8,7 @@ namespace GitHub.Copilot.Test.E2E; +[Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] public class ModeHandlersE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "mode_handlers", output) { @@ -24,7 +25,7 @@ public async Task Should_Invoke_Exit_Plan_Mode_Handler_When_Model_Uses_Tool() TaskCreationOptions.RunContinuationsAsynchronously); await using var client = CreateAuthenticatedClient(); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { GitHubToken = Token, OnPermissionRequest = PermissionHandler.ApproveAll, @@ -60,7 +61,7 @@ public async Task Should_Invoke_Exit_Plan_Mode_Handler_When_Model_Uses_Tool() var (request, invocation) = await handlerTask.Task.WaitAsync(TimeSpan.FromSeconds(30)); Assert.Equal(session.SessionId, invocation.SessionId); Assert.Equal(summary, request.Summary); - Assert.Equal(["interactive", "autopilot", "exit_only"], request.Actions); + Assert.Equal(["autopilot", "interactive", "exit_only"], request.Actions); Assert.Equal("interactive", request.RecommendedAction); Assert.NotNull(request.PlanContent); @@ -92,7 +93,7 @@ public async Task Should_Invoke_Auto_Mode_Switch_Handler_When_Rate_Limited() TaskCreationOptions.RunContinuationsAsynchronously); await using var client = CreateAuthenticatedClient(); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { GitHubToken = Token, OnPermissionRequest = PermissionHandler.ApproveAll, diff --git a/dotnet/test/E2E/MultiClientCommandsElicitationE2ETests.cs b/dotnet/test/E2E/MultiClientCommandsElicitationE2ETests.cs index d60c21709c..d869dc8164 100644 --- a/dotnet/test/E2E/MultiClientCommandsElicitationE2ETests.cs +++ b/dotnet/test/E2E/MultiClientCommandsElicitationE2ETests.cs @@ -59,7 +59,7 @@ public async Task InitializeAsync() await Ctx.ConfigureForTestAsync("multi_client", _testName); // Trigger connection so we can read the port - var initSession = await Client1.CreateSessionAsync(new SessionConfig + var initSession = await Ctx.CreateSessionAsync(Client1, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -102,7 +102,7 @@ public async Task DisposeAsync() [Fact] public async Task Client_Receives_Commands_Changed_When_Another_Client_Joins_With_Commands() { - var session1 = await Client1.CreateSessionAsync(new SessionConfig + var session1 = await Ctx.CreateSessionAsync(Client1, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -120,7 +120,7 @@ public async Task Client_Receives_Commands_Changed_When_Another_Client_Joins_Wit }); // Client2 joins with commands - var session2 = await Client2.ResumeSessionAsync(session1.SessionId, new ResumeSessionConfig + var session2 = await Ctx.ResumeSessionAsync(Client2, session1.SessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Commands = @@ -148,7 +148,7 @@ public async Task Client_Receives_Commands_Changed_When_Another_Client_Joins_Wit public async Task Capabilities_Changed_Fires_When_Second_Client_Joins_With_Elicitation_Handler() { // Client1 creates session without elicitation - var session1 = await Client1.CreateSessionAsync(new SessionConfig + var session1 = await Ctx.CreateSessionAsync(Client1, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -168,7 +168,7 @@ public async Task Capabilities_Changed_Fires_When_Second_Client_Joins_With_Elici }); // Client2 joins WITH elicitation handler — triggers capabilities.changed - var session2 = await Client2.ResumeSessionAsync(session1.SessionId, new ResumeSessionConfig + var session2 = await Ctx.ResumeSessionAsync(Client2, session1.SessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, OnElicitationRequest = _ => Task.FromResult(new ElicitationResult @@ -194,7 +194,7 @@ public async Task Capabilities_Changed_Fires_When_Second_Client_Joins_With_Elici public async Task Capabilities_Changed_Fires_When_Elicitation_Provider_Disconnects() { // Client1 creates session without elicitation - var session1 = await Client1.CreateSessionAsync(new SessionConfig + var session1 = await Ctx.CreateSessionAsync(Client1, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -222,7 +222,7 @@ public async Task Capabilities_Changed_Fires_When_Elicitation_Provider_Disconnec }); // Client3 joins WITH elicitation handler - await _client3.ResumeSessionAsync(session1.SessionId, new ResumeSessionConfig + await Ctx.ResumeSessionAsync(_client3, session1.SessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, OnElicitationRequest = _ => Task.FromResult(new ElicitationResult diff --git a/dotnet/test/E2E/MultiClientE2ETests.cs b/dotnet/test/E2E/MultiClientE2ETests.cs index faaf383935..4dbe7190ad 100644 --- a/dotnet/test/E2E/MultiClientE2ETests.cs +++ b/dotnet/test/E2E/MultiClientE2ETests.cs @@ -58,7 +58,7 @@ public async Task InitializeAsync() await Ctx.ConfigureForTestAsync("multi_client", _testName); // Trigger connection so we can read the port - var initSession = await Client1.CreateSessionAsync(new SessionConfig + var initSession = await Ctx.CreateSessionAsync(Client1, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -96,13 +96,13 @@ public async Task Both_Clients_See_Tool_Request_And_Completion_Events() { var tool = AIFunctionFactory.Create(MagicNumber, "magic_number"); - var session1 = await Client1.CreateSessionAsync(new SessionConfig + var session1 = await Ctx.CreateSessionAsync(Client1, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Tools = [tool], }); - var session2 = await Client2.ResumeSessionAsync(session1.SessionId, new ResumeSessionConfig + var session2 = await Ctx.ResumeSessionAsync(Client2, session1.SessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -148,7 +148,7 @@ public async Task One_Client_Approves_Permission_And_Both_See_The_Result() { var client1PermissionRequests = new List(); - var session1 = await Client1.CreateSessionAsync(new SessionConfig + var session1 = await Ctx.CreateSessionAsync(Client1, new SessionConfig { OnPermissionRequest = (request, _) => { @@ -158,7 +158,7 @@ public async Task One_Client_Approves_Permission_And_Both_See_The_Result() }); // Client 2 resumes — its handler never completes, so only client 1's approval takes effect - var session2 = await Client2.ResumeSessionAsync(session1.SessionId, new ResumeSessionConfig + var session2 = await Ctx.ResumeSessionAsync(Client2, session1.SessionId, new ResumeSessionConfig { OnPermissionRequest = (_, _) => new TaskCompletionSource().Task, }); @@ -200,13 +200,13 @@ await session1.SendAsync(new MessageOptions [Fact] public async Task One_Client_Rejects_Permission_And_Both_See_The_Result() { - var session1 = await Client1.CreateSessionAsync(new SessionConfig + var session1 = await Ctx.CreateSessionAsync(Client1, new SessionConfig { OnPermissionRequest = (_, _) => Task.FromResult(PermissionDecision.Reject()), }); // Client 2 resumes — its handler never completes - var session2 = await Client2.ResumeSessionAsync(session1.SessionId, new ResumeSessionConfig + var session2 = await Ctx.ResumeSessionAsync(Client2, session1.SessionId, new ResumeSessionConfig { OnPermissionRequest = (_, _) => new TaskCompletionSource().Task, }); @@ -252,13 +252,13 @@ public async Task Two_Clients_Register_Different_Tools_And_Agent_Uses_Both() var toolA = AIFunctionFactory.Create(CityLookup, "city_lookup"); var toolB = AIFunctionFactory.Create(CurrencyLookup, "currency_lookup"); - var session1 = await Client1.CreateSessionAsync(new SessionConfig + var session1 = await Ctx.CreateSessionAsync(Client1, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Tools = [toolA], }); - var session2 = await Client2.ResumeSessionAsync(session1.SessionId, new ResumeSessionConfig + var session2 = await Ctx.ResumeSessionAsync(Client2, session1.SessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Tools = [toolB], @@ -294,13 +294,13 @@ public async Task Disconnecting_Client_Removes_Its_Tools() var toolA = AIFunctionFactory.Create(StableTool, "stable_tool"); var toolB = AIFunctionFactory.Create(EphemeralTool, "ephemeral_tool"); - var session1 = await Client1.CreateSessionAsync(new SessionConfig + var session1 = await Ctx.CreateSessionAsync(Client1, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Tools = [toolA], }); - await Client2.ResumeSessionAsync(session1.SessionId, new ResumeSessionConfig + await Ctx.ResumeSessionAsync(Client2, session1.SessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Tools = [toolB], diff --git a/dotnet/test/E2E/MultiProviderRegistryE2ETests.cs b/dotnet/test/E2E/MultiProviderRegistryE2ETests.cs index 8a75cc3c1a..80827cc867 100644 --- a/dotnet/test/E2E/MultiProviderRegistryE2ETests.cs +++ b/dotnet/test/E2E/MultiProviderRegistryE2ETests.cs @@ -18,6 +18,7 @@ namespace GitHub.Copilot.Test.E2E; /// session, be launched, and route inference to the configured provider with /// the configured wire model and headers. /// +[Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public class MultiProviderRegistryE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "multi_provider_registry", output) { diff --git a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs index b330795905..b3ca218190 100644 --- a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs +++ b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs @@ -30,7 +30,7 @@ public async Task Should_Continue_Pending_Permission_Request_After_Resume() var cliUrl = GetCliUrl(server); using var suspendedClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: SharedToken) }); - var session1 = await suspendedClient.CreateSessionAsync(new SessionConfig + var session1 = await Ctx.CreateSessionAsync(suspendedClient, new SessionConfig { Tools = [AIFunctionFactory.Create(ResumePermissionTool, "resume_permission_tool")], OnPermissionRequest = (request, _) => @@ -57,7 +57,7 @@ await session1.SendAsync(new MessageOptions await suspendedClient.ForceStopAsync(); await using var resumedTcpClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: SharedToken) }); - var session2 = await resumedTcpClient.ResumeSessionAsync(sessionId, new ResumeSessionConfig + var session2 = await Ctx.ResumeSessionAsync(resumedTcpClient, sessionId, new ResumeSessionConfig { ContinuePendingWork = true, OnPermissionRequest = (_, _) => Task.FromResult(PermissionDecision.NoResult()), @@ -99,7 +99,7 @@ public async Task Should_Continue_Pending_External_Tool_Request_After_Resume() var cliUrl = GetCliUrl(server); using var suspendedClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: SharedToken) }); - var session1 = await suspendedClient.CreateSessionAsync(new SessionConfig + var session1 = await Ctx.CreateSessionAsync(suspendedClient, new SessionConfig { Tools = [AIFunctionFactory.Create(BlockingExternalTool, "resume_external_tool")], OnPermissionRequest = PermissionHandler.ApproveAll, @@ -120,7 +120,7 @@ await session1.SendAsync(new MessageOptions await suspendedClient.ForceStopAsync(); await using var resumedClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: SharedToken) }); - var session2 = await resumedClient.ResumeSessionAsync(sessionId, new ResumeSessionConfig + var session2 = await Ctx.ResumeSessionAsync(resumedClient, sessionId, new ResumeSessionConfig { ContinuePendingWork = true, OnPermissionRequest = PermissionHandler.ApproveAll, @@ -175,7 +175,7 @@ private async Task AssertPendingExternalToolHandleableOnResumeAsync( var cliUrl = GetCliUrl(server); using var suspendedClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: SharedToken) }); - var session1 = await suspendedClient.CreateSessionAsync(new SessionConfig + var session1 = await Ctx.CreateSessionAsync(suspendedClient, new SessionConfig { Tools = [AIFunctionFactory.Create(BlockingExternalTool, "resume_external_tool")], OnPermissionRequest = PermissionHandler.ApproveAll, @@ -216,7 +216,7 @@ await session1.SendAsync(new MessageOptions resumeConfig.Tools = [AIFunctionFactory.Create(ResumedExternalTool, "resume_external_tool")]; } - var session2 = await resumedClient.ResumeSessionAsync(sessionId, resumeConfig); + var session2 = await Ctx.ResumeSessionAsync(resumedClient, sessionId, resumeConfig); var resumeEvent = await GetSingleResumeEventAsync(session2); Assert.Equal(false, resumeEvent.Data.ContinuePendingWork); @@ -276,7 +276,7 @@ public async Task Should_Continue_Parallel_Pending_External_Tool_Requests_After_ var cliUrl = GetCliUrl(server); using var suspendedClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: SharedToken) }); - var session1 = await suspendedClient.CreateSessionAsync(new SessionConfig + var session1 = await Ctx.CreateSessionAsync(suspendedClient, new SessionConfig { Tools = [ @@ -306,7 +306,7 @@ await Task.WhenAll( await suspendedClient.ForceStopAsync(); await using var resumedClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: SharedToken) }); - var session2 = await resumedClient.ResumeSessionAsync(sessionId, new ResumeSessionConfig + var session2 = await Ctx.ResumeSessionAsync(resumedClient, sessionId, new ResumeSessionConfig { ContinuePendingWork = true, OnPermissionRequest = PermissionHandler.ApproveAll, @@ -357,7 +357,7 @@ public async Task Should_Resume_Successfully_When_No_Pending_Work_Exists() string sessionId; await using (var firstClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: SharedToken) })) { - var firstSession = await firstClient.CreateSessionAsync(new SessionConfig + var firstSession = await Ctx.CreateSessionAsync(firstClient, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -370,7 +370,7 @@ public async Task Should_Resume_Successfully_When_No_Pending_Work_Exists() } await using var resumedClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: SharedToken) }); - var resumedSession = await resumedClient.ResumeSessionAsync(sessionId, new ResumeSessionConfig + var resumedSession = await Ctx.ResumeSessionAsync(resumedClient, sessionId, new ResumeSessionConfig { ContinuePendingWork = true, OnPermissionRequest = PermissionHandler.ApproveAll, @@ -395,7 +395,7 @@ public async Task Should_Report_ContinuePendingWork_True_In_Resume_Event() string sessionId; await using (var firstClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: SharedToken) })) { - var firstSession = await firstClient.CreateSessionAsync(new SessionConfig + var firstSession = await Ctx.CreateSessionAsync(firstClient, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -411,7 +411,7 @@ public async Task Should_Report_ContinuePendingWork_True_In_Resume_Event() } await using var resumedClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: SharedToken) }); - var resumedSession = await resumedClient.ResumeSessionAsync(sessionId, new ResumeSessionConfig + var resumedSession = await Ctx.ResumeSessionAsync(resumedClient, sessionId, new ResumeSessionConfig { ContinuePendingWork = true, OnPermissionRequest = PermissionHandler.ApproveAll, diff --git a/dotnet/test/E2E/PerSessionAuthE2ETests.cs b/dotnet/test/E2E/PerSessionAuthE2ETests.cs index d1226f3737..4b370768a1 100644 --- a/dotnet/test/E2E/PerSessionAuthE2ETests.cs +++ b/dotnet/test/E2E/PerSessionAuthE2ETests.cs @@ -8,6 +8,7 @@ namespace GitHub.Copilot.Test.E2E; +[Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] public class PerSessionAuthE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "per-session-auth", output) { /// @@ -74,7 +75,7 @@ public async Task ShouldAuthenticateWithGitHubToken() { await SetupCopilotUsersAsync(); - await using var session = await AuthClient.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(AuthClient, new SessionConfig { GitHubToken = "token-alice", OnPermissionRequest = PermissionHandler.ApproveAll, @@ -90,13 +91,13 @@ public async Task ShouldIsolateAuthBetweenSessions() { await SetupCopilotUsersAsync(); - await using var sessionA = await AuthClient.CreateSessionAsync(new SessionConfig + await using var sessionA = await Ctx.CreateSessionAsync(AuthClient, new SessionConfig { GitHubToken = "token-alice", OnPermissionRequest = PermissionHandler.ApproveAll, }); - await using var sessionB = await AuthClient.CreateSessionAsync(new SessionConfig + await using var sessionB = await Ctx.CreateSessionAsync(AuthClient, new SessionConfig { GitHubToken = "token-bob", OnPermissionRequest = PermissionHandler.ApproveAll, @@ -116,7 +117,7 @@ public async Task ShouldBeUnauthenticatedWithoutToken() { var noAuthClient = CreateNoAuthTestClient(); - await using var session = await noAuthClient.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(noAuthClient, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -131,7 +132,7 @@ public async Task ShouldFailWithInvalidToken() { await SetupCopilotUsersAsync(); - var ex = await Assert.ThrowsAnyAsync(() => AuthClient.CreateSessionAsync(new SessionConfig + var ex = await Assert.ThrowsAnyAsync(() => Ctx.CreateSessionAsync(AuthClient, new SessionConfig { GitHubToken = "invalid-token", OnPermissionRequest = PermissionHandler.ApproveAll, diff --git a/dotnet/test/E2E/PermissionE2ETests.cs b/dotnet/test/E2E/PermissionE2ETests.cs index 8c1904701a..2225dcba89 100644 --- a/dotnet/test/E2E/PermissionE2ETests.cs +++ b/dotnet/test/E2E/PermissionE2ETests.cs @@ -205,7 +205,7 @@ public async Task Should_Resume_Session_With_Permission_Handler() await session1.DisposeAsync(); // Resume with permission handler - var session2 = await Client.ResumeSessionAsync(sessionId, new ResumeSessionConfig + var session2 = await Ctx.ResumeSessionAsync(Client, sessionId, new ResumeSessionConfig { OnPermissionRequest = (request, invocation) => { @@ -279,7 +279,7 @@ public async Task Should_Deny_Tool_Operations_When_Handler_Explicitly_Denies_Aft await session1.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" }); await session1.DisposeAsync(); - var session2 = await Client.ResumeSessionAsync(sessionId, new ResumeSessionConfig + var session2 = await Ctx.ResumeSessionAsync(Client, sessionId, new ResumeSessionConfig { OnPermissionRequest = (_, _) => Task.FromResult(PermissionDecision.UserNotAvailable()) diff --git a/dotnet/test/E2E/ProviderEndpointE2ETests.cs b/dotnet/test/E2E/ProviderEndpointE2ETests.cs index 7ea4eecf39..d2bf06982f 100644 --- a/dotnet/test/E2E/ProviderEndpointE2ETests.cs +++ b/dotnet/test/E2E/ProviderEndpointE2ETests.cs @@ -27,11 +27,12 @@ private CopilotClient CreateProviderEndpointClient() } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task ShouldReturnByokProviderEndpointWhenCustomProviderIsConfigured() { var client = CreateProviderEndpointClient(); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Provider = new ProviderConfig @@ -64,11 +65,12 @@ public async Task ShouldReturnByokProviderEndpointWhenCustomProviderIsConfigured } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] public async Task ShouldReturnCapiProviderEndpointForOAuthAuthenticatedSession() { var client = CreateProviderEndpointClient(); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); diff --git a/dotnet/test/E2E/RpcExtensionsLoadedE2ETests.cs b/dotnet/test/E2E/RpcExtensionsLoadedE2ETests.cs index af959bd7eb..0a3513e4b5 100644 --- a/dotnet/test/E2E/RpcExtensionsLoadedE2ETests.cs +++ b/dotnet/test/E2E/RpcExtensionsLoadedE2ETests.cs @@ -189,7 +189,7 @@ public async Task Discovers_Loads_And_Reports_Running_Extension(string sourceVal await using var client = CreateExtensionsClient(); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { EnableConfigDiscovery = true, WorkingDirectory = workingDirectory, @@ -214,7 +214,7 @@ public async Task Disable_Then_Enable_Cycles_Extension_Status() await using var client = CreateExtensionsClient(); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { EnableConfigDiscovery = true, OnPermissionRequest = PermissionHandler.ApproveAll, @@ -240,7 +240,7 @@ 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 client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { EnableConfigDiscovery = true, OnPermissionRequest = PermissionHandler.ApproveAll, @@ -285,7 +285,7 @@ public async Task Failed_Extension_Reports_Failed_Status() await using var client = CreateExtensionsClient(); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { EnableConfigDiscovery = true, OnPermissionRequest = PermissionHandler.ApproveAll, @@ -306,7 +306,7 @@ public async Task Multiple_Extensions_Are_Discovered_Independently() await using var client = CreateExtensionsClient(); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { EnableConfigDiscovery = true, OnPermissionRequest = PermissionHandler.ApproveAll, @@ -328,7 +328,7 @@ public async Task Reload_Preserves_Disabled_State_Across_Calls() await using var client = CreateExtensionsClient(); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { EnableConfigDiscovery = true, OnPermissionRequest = PermissionHandler.ApproveAll, diff --git a/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs b/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs index 350aac4274..0d2942d4bf 100644 --- a/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs +++ b/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs @@ -189,7 +189,7 @@ public async Task Should_List_Extensions() { Connection = RuntimeConnection.ForStdio(args: ["--yolo"]), }); - await using var session = await yoloClient.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(yoloClient, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -208,7 +208,7 @@ public async Task Should_List_Extensions() public async Task Should_Round_Trip_Mcp_App_Host_Context() { await using var client = CreateMcpAppsClient(); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -253,7 +253,7 @@ public async Task Should_Diagnose_And_Report_Mcp_App_Capability_Errors() new Dictionary { ["MCP_APP_RPC_VALUE"] = "from-app-rpc" }; await using var client = CreateMcpAppsClient(); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { McpServers = mcpServers, OnPermissionRequest = PermissionHandler.ApproveAll, @@ -292,7 +292,7 @@ public async Task Should_Report_Error_When_Mcp_App_Resource_Is_Not_Available() { const string serverName = "rpc-apps-resource-server"; await using var client = CreateMcpAppsClient(); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { McpServers = CreateTestMcpServers(serverName), OnPermissionRequest = PermissionHandler.ApproveAll, @@ -368,7 +368,7 @@ public async Task Should_Report_Error_When_Extensions_Are_Not_Available() { Connection = RuntimeConnection.ForStdio(args: ["--yolo"]), }); - await using var session = await yoloClient.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(yoloClient, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); diff --git a/dotnet/test/E2E/RpcServerE2ETests.cs b/dotnet/test/E2E/RpcServerE2ETests.cs index 47df256157..2df8593cc4 100644 --- a/dotnet/test/E2E/RpcServerE2ETests.cs +++ b/dotnet/test/E2E/RpcServerE2ETests.cs @@ -160,6 +160,7 @@ public async Task Should_Reject_Llm_Inference_Response_Frames_For_Missing_Reques } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] public async Task Should_Call_Rpc_Models_List_With_Typed_Result() { const string token = "rpc-models-token"; @@ -175,6 +176,7 @@ public async Task Should_Call_Rpc_Models_List_With_Typed_Result() } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] public async Task Should_Call_Rpc_Account_GetQuota_When_Authenticated() { const string token = "rpc-quota-token"; @@ -256,7 +258,7 @@ public async Task Should_List_Find_And_Inspect_Persisted_Session_State() var missingTaskId = $"missing-task-{Guid.NewGuid():N}"; var missingSessionId = Guid.NewGuid().ToString(); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { SessionId = sessionId, WorkingDirectory = workingDirectory, @@ -308,7 +310,7 @@ public async Task Should_Enrich_Basic_Session_Metadata() var sessionId = Guid.NewGuid().ToString(); var workingDirectory = CreateUniqueWorkDirectory("server-rpc-enrich"); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { SessionId = sessionId, WorkingDirectory = workingDirectory, @@ -353,7 +355,7 @@ public async Task Should_Close_Active_Session_And_Release_Lock() await using var client = CreateAuthenticatedClient(token); var sessionId = Guid.NewGuid().ToString(); var workingDirectory = CreateUniqueWorkDirectory("server-rpc-close"); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { SessionId = sessionId, WorkingDirectory = workingDirectory, @@ -378,7 +380,7 @@ public async Task Should_Check_In_Use_Session_From_Another_Runtime_And_Release_L var sessionId = Guid.NewGuid().ToString(); var workingDirectory = CreateUniqueWorkDirectory("server-rpc-in-use"); await using var otherClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForStdio() }); - await using var otherSession = await otherClient.CreateSessionAsync(new SessionConfig + await using var otherSession = await Ctx.CreateSessionAsync(otherClient, new SessionConfig { SessionId = sessionId, WorkingDirectory = workingDirectory, @@ -419,7 +421,7 @@ public async Task Should_Prune_DryRun_And_BulkDelete_Persisted_Session() var missingSessionId = Guid.NewGuid().ToString(); var workingDirectory = CreateUniqueWorkDirectory("server-rpc-delete"); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { SessionId = sessionId, WorkingDirectory = workingDirectory, diff --git a/dotnet/test/E2E/RpcSessionStateE2ETests.cs b/dotnet/test/E2E/RpcSessionStateE2ETests.cs index 93af399203..dfd76fb34f 100644 --- a/dotnet/test/E2E/RpcSessionStateE2ETests.cs +++ b/dotnet/test/E2E/RpcSessionStateE2ETests.cs @@ -46,7 +46,7 @@ public async Task Should_Call_Session_Rpc_Model_SwitchTo() await isolatedCtx.ConfigureForTestAsync("rpc_session_state", nameof(Should_Call_Session_Rpc_Model_SwitchTo)); var isolatedClient = isolatedCtx.CreateClient(); - await using var session = await isolatedClient.CreateSessionAsync(new SessionConfig + await using var session = await isolatedCtx.CreateSessionAsync(isolatedClient, new SessionConfig { Model = "claude-sonnet-4.5", OnPermissionRequest = PermissionHandler.ApproveAll, @@ -443,7 +443,7 @@ public async Task Should_Set_ReasoningEffort_And_Auto_Name() public async Task Should_Set_Auth_Credentials() { await using var client = Ctx.CreateClient(); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); diff --git a/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs b/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs index f3f3d5d5de..28ec9b7cfe 100644 --- a/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs +++ b/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs @@ -20,6 +20,7 @@ public class RpcSessionStateExtrasE2ETests(E2ETestFixture fixture, ITestOutputHe : E2ETestBase(fixture, "rpc_session_state_extras", output) { [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] public async Task Should_List_Models_For_Session() { // model.list resolves models through the session's own auth context, which requires the @@ -29,7 +30,7 @@ public async Task Should_List_Models_For_Session() const string token = "rpc-session-model-list-token"; await ConfigureAuthenticatedUserAsync(token); await using var client = CreateAuthenticatedClient(token); - await using var session = await client.CreateSessionAsync(new SessionConfig + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { Model = "claude-sonnet-4.5", OnPermissionRequest = PermissionHandler.ApproveAll, @@ -44,6 +45,7 @@ public async Task Should_List_Models_For_Session() } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task Should_Add_Byok_Provider_And_Model_At_Runtime() { await using var session = await CreateSessionAsync(); diff --git a/dotnet/test/E2E/RpcTasksAndHandlersE2ETests.cs b/dotnet/test/E2E/RpcTasksAndHandlersE2ETests.cs index 9b9738a2ee..989fef7c55 100644 --- a/dotnet/test/E2E/RpcTasksAndHandlersE2ETests.cs +++ b/dotnet/test/E2E/RpcTasksAndHandlersE2ETests.cs @@ -72,6 +72,9 @@ await AssertImplementedFailureAsync( } [Fact] + // TODO(BYOK): Provider-backed task agents handled an invalid model differently. Verify that + // BYOK model validation should reject it consistently before keeping this CAPI-only. + [Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] public async Task Should_Report_Implemented_Error_For_Invalid_Task_Agent_Model() { var session = await CreateSessionAsync(); diff --git a/dotnet/test/E2E/SessionConfigE2ETests.cs b/dotnet/test/E2E/SessionConfigE2ETests.cs index 45cdef2016..ad313b116d 100644 --- a/dotnet/test/E2E/SessionConfigE2ETests.cs +++ b/dotnet/test/E2E/SessionConfigE2ETests.cs @@ -22,6 +22,9 @@ public class SessionConfigE2ETests(E2ETestFixture fixture, ITestOutputHelper out "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="); [Fact] + // TODO(BYOK): Anthropic Messages history diverged after enabling vision via SetModel. Verify + // that model capability overrides work for provider-backed sessions before keeping this CAPI-only. + [Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] public async Task Vision_Disabled_Then_Enabled_Via_SetModel() { await File.WriteAllBytesAsync(Path.Join(Ctx.WorkDir, "test.png"), Png1X1); @@ -62,6 +65,9 @@ await session.SetModelAsync( } [Fact] + // TODO(BYOK): Anthropic Messages history diverged after disabling vision via SetModel. Verify + // that model capability overrides work for provider-backed sessions before keeping this CAPI-only. + [Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] public async Task Vision_Enabled_Then_Disabled_Via_SetModel() { await File.WriteAllBytesAsync(Path.Join(Ctx.WorkDir, "test.png"), Png1X1); @@ -121,6 +127,7 @@ public async Task Should_Use_Custom_SessionId() } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task Should_Apply_ReasoningEffort_On_Session_Create() { const string reasoningModelId = "custom-reasoning-model"; @@ -140,6 +147,7 @@ public async Task Should_Apply_ReasoningEffort_On_Session_Create() } [Theory] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] [InlineData("low")] [InlineData("medium")] [InlineData("high")] @@ -162,6 +170,7 @@ public async Task Should_Apply_All_ReasoningEffort_Values_On_Session_Create(stri } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task Should_Apply_ReasoningEffort_On_Session_Resume() { var originalSession = await CreateSessionAsync(); @@ -182,6 +191,9 @@ public async Task Should_Apply_ReasoningEffort_On_Session_Resume() } [Fact] + // TODO(BYOK): The Anthropic user-agent omitted ClientName and contained only its provider SDK + // identifier. Determine the expected propagation for custom providers before keeping this CAPI-only. + [Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] public async Task Should_Forward_ClientName_In_UserAgent() { var session = await CreateSessionAsync(new SessionConfig @@ -198,6 +210,7 @@ public async Task Should_Forward_ClientName_In_UserAgent() } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task Should_Forward_Custom_Provider_Headers_On_Create() { var session = await CreateSessionAsync(new SessionConfig @@ -217,6 +230,7 @@ public async Task Should_Forward_Custom_Provider_Headers_On_Create() } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task Should_Forward_Custom_Provider_Headers_On_Resume() { var session1 = await CreateSessionAsync(); @@ -239,6 +253,7 @@ public async Task Should_Forward_Custom_Provider_Headers_On_Resume() } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task Should_Forward_Provider_Wire_Model() { // Verifies that ProviderConfig.WireModel overrides the model name sent to @@ -270,6 +285,7 @@ public async Task Should_Forward_Provider_Wire_Model() } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task Should_Use_Provider_Model_Id_As_Wire_Model() { // ProviderConfig.ModelId drives both the runtime resolved model AND the wire model @@ -564,13 +580,14 @@ public async Task Should_Apply_Excluded_Built_In_Agents_On_Resume() } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task Should_Enable_Citations_For_Anthropic_File_Attachments_On_Create() { var handler = new RecordingRequestHandler(); await using var client = CreateClientWithRequestHandler(handler); await client.StartAsync(); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-sonnet-4.5", @@ -595,6 +612,7 @@ await session.SendAndWaitAsync(new MessageOptions } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task Should_Enable_Citations_For_Anthropic_File_Attachments_On_Resume() { const string connectionToken = "citation-resume-token"; @@ -604,7 +622,7 @@ public async Task Should_Enable_Citations_For_Anthropic_File_Attachments_On_Resu RuntimeConnection.ForTcp(connectionToken: connectionToken)); await client.StartAsync(); - var session1 = await client.CreateSessionAsync(new SessionConfig + var session1 = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -616,7 +634,7 @@ public async Task Should_Enable_Citations_For_Anthropic_File_Attachments_On_Resu Connection = RuntimeConnection.ForUri($"localhost:{port}", connectionToken: connectionToken), }); - var session2 = await resumeClient.ResumeSessionAsync(sessionId, new ResumeSessionConfig + var session2 = await Ctx.ResumeSessionAsync(resumeClient, sessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-sonnet-4.5", @@ -642,6 +660,7 @@ await session2.SendAndWaitAsync(new MessageOptions } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task Should_Create_Session_With_Custom_Provider_Config() { // Per the TS test (session_config.e2e.test.ts), this only verifies that a @@ -669,6 +688,9 @@ public async Task Should_Create_Session_With_Custom_Provider_Config() } [Fact] + // TODO(BYOK): Anthropic Messages request history diverged while replaying this blob attachment. + // Confirm native clients preserve blob/image turns before keeping this CAPI-only. + [Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] public async Task Should_Accept_Blob_Attachments() { // Write the image to disk so the model can view it if it tries diff --git a/dotnet/test/E2E/SessionE2ETests.cs b/dotnet/test/E2E/SessionE2ETests.cs index bcfb46295a..bcc8fc268d 100644 --- a/dotnet/test/E2E/SessionE2ETests.cs +++ b/dotnet/test/E2E/SessionE2ETests.cs @@ -232,7 +232,7 @@ public async Task Should_Reject_Resuming_Active_Session_Using_The_Same_Client() var sessionId = session1.SessionId; var exception = await Assert.ThrowsAsync(() => - Client.ResumeSessionAsync(sessionId, new ResumeSessionConfig + Ctx.ResumeSessionAsync(Client, sessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, })); @@ -251,7 +251,7 @@ public async Task Should_Resume_A_Session_Using_A_New_Client() Assert.Contains("2", answer!.Data.Content ?? string.Empty); using var newClient = Ctx.CreateClient(); - var session2 = await newClient.ResumeSessionAsync(sessionId, new ResumeSessionConfig + var session2 = await Ctx.ResumeSessionAsync(newClient, sessionId, new ResumeSessionConfig { ContinuePendingWork = true, OnPermissionRequest = PermissionHandler.ApproveAll, @@ -287,7 +287,7 @@ public async Task Resumes_A_Persisted_Session_From_A_New_Client_When_An_Mcp_OAut Assert.Contains("2", answer!.Data.Content ?? string.Empty); using var newClient = Ctx.CreateClient(); - await using var session2 = await newClient.ResumeSessionAsync(sessionId, new ResumeSessionConfig + await using var session2 = await Ctx.ResumeSessionAsync(newClient, sessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, OnMcpAuthRequest = CancelMcpAuthAsync, @@ -732,6 +732,9 @@ public async Task DisposeAsync_From_Handler_Does_Not_Deadlock() } [Fact] + // TODO(BYOK): Anthropic Messages request history diverged while replaying this blob attachment. + // Confirm native clients preserve blob/image turns before keeping this CAPI-only. + [Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] public async Task Should_Accept_Blob_Attachments() { var pngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; @@ -922,6 +925,7 @@ await session.SendAndWaitAsync(new MessageOptions } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task Should_Create_Session_With_Custom_Provider() { var session = await CreateSessionAsync(new SessionConfig @@ -947,6 +951,7 @@ public async Task Should_Create_Session_With_Custom_Provider() } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task Should_Create_Session_With_Azure_Provider() { var session = await CreateSessionAsync(new SessionConfig @@ -976,6 +981,7 @@ public async Task Should_Create_Session_With_Azure_Provider() } [Fact] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] public async Task Should_Resume_Session_With_Custom_Provider() { var session = await CreateSessionAsync(); diff --git a/dotnet/test/E2E/SessionFsE2ETests.cs b/dotnet/test/E2E/SessionFsE2ETests.cs index b1b4848adc..cf91e3ddc8 100644 --- a/dotnet/test/E2E/SessionFsE2ETests.cs +++ b/dotnet/test/E2E/SessionFsE2ETests.cs @@ -28,7 +28,7 @@ public async Task Should_Route_File_Operations_Through_The_Session_Fs_Provider() { await using var client = CreateSessionFsClient(providerRoot); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, CreateSessionFsProvider = s => new TestSessionFsHandler(s.SessionId, providerRoot), @@ -58,7 +58,7 @@ public async Task Should_Load_Session_Data_From_Fs_Provider_On_Resume() await using var client = CreateSessionFsClient(providerRoot); Func createSessionFsHandler = s => new TestSessionFsHandler(s.SessionId, providerRoot); - var session1 = await client.CreateSessionAsync(new SessionConfig + var session1 = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, CreateSessionFsProvider = createSessionFsHandler, @@ -72,7 +72,7 @@ public async Task Should_Load_Session_Data_From_Fs_Provider_On_Resume() var eventsPath = GetStoredPath(providerRoot, sessionId, $"{SessionFsConfig.SessionStatePath}/events.jsonl"); await WaitForConditionAsync(() => File.Exists(eventsPath)); - var session2 = await client.ResumeSessionAsync(sessionId, new ResumeSessionConfig + var session2 = await Ctx.ResumeSessionAsync(client, sessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, CreateSessionFsProvider = createSessionFsHandler, @@ -97,7 +97,7 @@ public async Task Should_Reject_SetProvider_When_Sessions_Already_Exist() await using var client1 = CreateSessionFsClient(providerRoot, useStdio: false, tcpConnectionToken: "session-fs-shared-token"); var createSessionFsHandler = (Func)(s => new TestSessionFsHandler(s.SessionId, providerRoot)); - _ = await client1.CreateSessionAsync(new SessionConfig + _ = await Ctx.CreateSessionAsync(client1, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, CreateSessionFsProvider = createSessionFsHandler, @@ -319,7 +319,7 @@ public async Task Should_Map_Large_Output_Handling_Into_SessionFs() var suppliedFileContent = new string('x', largeContentSize); await using var client = CreateSessionFsClient(providerRoot); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, CreateSessionFsProvider = s => new TestSessionFsHandler(s.SessionId, providerRoot), @@ -361,7 +361,7 @@ public async Task Should_Succeed_With_Compaction_While_Using_SessionFs() try { await using var client = CreateSessionFsClient(providerRoot); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, CreateSessionFsProvider = s => new TestSessionFsHandler(s.SessionId, providerRoot), @@ -400,7 +400,7 @@ public async Task Should_Write_Workspace_Metadata_Via_SessionFs() try { await using var client = CreateSessionFsClient(providerRoot); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, CreateSessionFsProvider = s => new TestSessionFsHandler(s.SessionId, providerRoot), @@ -433,7 +433,7 @@ public async Task Should_Persist_Plan_Md_Via_SessionFs() try { await using var client = CreateSessionFsClient(providerRoot); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, CreateSessionFsProvider = s => new TestSessionFsHandler(s.SessionId, providerRoot), diff --git a/dotnet/test/E2E/SessionFsSqliteE2ETests.cs b/dotnet/test/E2E/SessionFsSqliteE2ETests.cs index bf7bdf3b5e..8ed86c72e3 100644 --- a/dotnet/test/E2E/SessionFsSqliteE2ETests.cs +++ b/dotnet/test/E2E/SessionFsSqliteE2ETests.cs @@ -31,7 +31,7 @@ public async Task Should_Route_Sql_Queries_Through_The_Sessionfs_Sqlite_Handler( { await using var client = CreateSessionFsClient(); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, CreateSessionFsProvider = s => new InMemorySessionFsSqliteHandler(s.SessionId, _sqliteCalls), @@ -65,7 +65,7 @@ public async Task Should_Allow_Subagents_To_Use_Sql_Tool_Via_Inherited_Sessionfs await using var client = CreateSessionFsClient(); var handler = (InMemorySessionFsSqliteHandler?)null; - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, CreateSessionFsProvider = s => diff --git a/dotnet/test/E2E/StreamingFidelityE2ETests.cs b/dotnet/test/E2E/StreamingFidelityE2ETests.cs index fec00f1cb1..4df9ca4420 100644 --- a/dotnet/test/E2E/StreamingFidelityE2ETests.cs +++ b/dotnet/test/E2E/StreamingFidelityE2ETests.cs @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using GitHub.Copilot.Test.Harness; using Xunit; using Xunit.Abstractions; @@ -46,6 +47,9 @@ public async Task Should_Produce_Delta_Events_When_Streaming_Is_Enabled() } [Fact] + // TODO(BYOK): Anthropic Messages emitted delta events with Streaming=false. Investigate the + // native-client streaming contract before keeping this disabled for every BYOK backend. + [Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] public async Task Should_Not_Produce_Deltas_When_Streaming_Is_Disabled() { var session = await CreateSessionAsync(new SessionConfig { Streaming = false }); @@ -79,7 +83,7 @@ public async Task Should_Produce_Deltas_After_Session_Resume() // Resume using a new client using var newClient = Ctx.CreateClient(); - var session2 = await newClient.ResumeSessionAsync(session.SessionId, + var session2 = await Ctx.ResumeSessionAsync(newClient, session.SessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Streaming = true }); var events = new List(); @@ -106,6 +110,9 @@ public async Task Should_Produce_Deltas_After_Session_Resume() } [Fact] + // TODO(BYOK): Anthropic Messages emitted delta events after resuming with Streaming=false. + // Investigate the native-client streaming contract before keeping this disabled for every BYOK backend. + [Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] public async Task Should_Not_Produce_Deltas_After_Session_Resume_With_Streaming_Disabled() { var session = await CreateSessionAsync(new SessionConfig { Streaming = true }); @@ -114,7 +121,7 @@ public async Task Should_Not_Produce_Deltas_After_Session_Resume_With_Streaming_ // Resume using a new client with streaming DISABLED using var newClient = Ctx.CreateClient(); - var session2 = await newClient.ResumeSessionAsync(session.SessionId, + var session2 = await Ctx.ResumeSessionAsync(newClient, session.SessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Streaming = false }); var events = new List(); diff --git a/dotnet/test/E2E/SubagentHooksE2ETests.cs b/dotnet/test/E2E/SubagentHooksE2ETests.cs index 4723c19b78..8314b8c098 100644 --- a/dotnet/test/E2E/SubagentHooksE2ETests.cs +++ b/dotnet/test/E2E/SubagentHooksE2ETests.cs @@ -3,12 +3,15 @@ *--------------------------------------------------------------------------------------------*/ using System.Collections.Concurrent; +using System.Net.Http; using GitHub.Copilot.Test.Harness; using Xunit; using Xunit.Abstractions; namespace GitHub.Copilot.Test.E2E; +#pragma warning disable GHCP001 // The LLM inference surface is intentionally experimental. + public class SubagentHooksE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "subagent_hooks", output) { @@ -16,13 +19,18 @@ public class SubagentHooksE2ETests(E2ETestFixture fixture, ITestOutputHelper out public async Task Should_Invoke_PreToolUse_And_PostToolUse_Hooks_For_Sub_Agent_Tool_Calls() { var hookLog = new ConcurrentBag<(string Kind, string ToolName, string SessionId)>(); + var requestHandler = new RecordingForwardingRequestHandler(); // Create a client with the session-based subagents feature flag var env = new Dictionary(Ctx.GetEnvironment()); env["COPILOT_EXP_COPILOT_CLI_SESSION_BASED_SUBAGENTS"] = "true"; - var client = Ctx.CreateClient(environment: env); + var client = Ctx.CreateClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForStdio(), + RequestHandler = requestHandler + }, environment: env); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Hooks = new SessionHooks @@ -69,5 +77,42 @@ await session.SendAndWaitAsync( // input.SessionId distinguishes parent from sub-agent Assert.NotEqual(viewPre[0].SessionId, taskPre[0].SessionId); + AssertSubagentRequestMetadata(requestHandler.InferenceRequests); + } + + private static void AssertSubagentRequestMetadata(IReadOnlyCollection records) + { + Assert.NotEmpty(records); + var subagentRequest = records.FirstOrDefault(r => !string.IsNullOrEmpty(r.ParentAgentId)); + Assert.NotNull(subagentRequest); + Assert.False(string.IsNullOrEmpty(subagentRequest.AgentId), + "Sub-agent inference request should carry an agent id"); + Assert.False(string.IsNullOrEmpty(subagentRequest.InteractionType), + "Sub-agent inference request should carry an interaction type"); + Assert.NotEqual(subagentRequest.ParentAgentId, subagentRequest.AgentId); + } + + private sealed class RecordingForwardingRequestHandler : CopilotRequestHandler + { + private readonly ConcurrentBag _records = []; + + public IReadOnlyCollection InferenceRequests => + [.. _records.Where(r => RecordingRequestHandler.IsInferenceUrl(r.Url))]; + + protected override Task SendRequestAsync(HttpRequestMessage request, CopilotRequestContext ctx) + { + _records.Add(new RequestRecord( + request.RequestUri!.ToString(), + ctx.AgentId, + ctx.ParentAgentId, + ctx.InteractionType)); + return base.SendRequestAsync(request, ctx); + } } + + private sealed record RequestRecord( + string Url, + string? AgentId, + string? ParentAgentId, + string? InteractionType); } diff --git a/dotnet/test/E2E/SuspendE2ETests.cs b/dotnet/test/E2E/SuspendE2ETests.cs index f531f0e59c..b88a9897be 100644 --- a/dotnet/test/E2E/SuspendE2ETests.cs +++ b/dotnet/test/E2E/SuspendE2ETests.cs @@ -58,7 +58,7 @@ public async Task Should_Allow_Resume_And_Continue_Conversation_After_Suspend() string sessionId; await using (var client1 = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: sharedToken) })) { - var session1 = await client1.CreateSessionAsync(new SessionConfig + var session1 = await Ctx.CreateSessionAsync(client1, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -78,7 +78,7 @@ await session1.SendAndWaitAsync(new MessageOptions // A different client should be able to pick the session back up. The previous // turn was completed before suspend, so there is no pending work to continue. await using var client2 = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: sharedToken) }); - var session2 = await client2.ResumeSessionAsync(sessionId, new ResumeSessionConfig + var session2 = await Ctx.ResumeSessionAsync(client2, sessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); diff --git a/dotnet/test/E2E/TelemetryExportE2ETests.cs b/dotnet/test/E2E/TelemetryExportE2ETests.cs index 2dd9d591a4..48e3b53ad6 100644 --- a/dotnet/test/E2E/TelemetryExportE2ETests.cs +++ b/dotnet/test/E2E/TelemetryExportE2ETests.cs @@ -34,7 +34,7 @@ public async Task Should_Export_File_Telemetry_For_Sdk_Interactions() }, }); - var session = await client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(client, new SessionConfig { Tools = [AIFunctionFactory.Create(EchoTelemetryMarker, toolName, "Echoes a marker string for telemetry validation.")], OnPermissionRequest = PermissionHandler.ApproveAll, diff --git a/dotnet/test/E2E/ToolsE2ETests.cs b/dotnet/test/E2E/ToolsE2ETests.cs index 7424cfa3a3..ea615fbc4a 100644 --- a/dotnet/test/E2E/ToolsE2ETests.cs +++ b/dotnet/test/E2E/ToolsE2ETests.cs @@ -306,7 +306,7 @@ public async Task Invokes_Custom_Tool_With_Permission_Handler() { var permissionRequests = new List(); - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(Client, new SessionConfig { Tools = [AIFunctionFactory.Create(EncryptStringForPermission, "encrypt_string")], OnPermissionRequest = (request, invocation) => @@ -340,7 +340,7 @@ public async Task Denies_Custom_Tool_When_Permission_Denied() { var toolHandlerCalled = false; - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await Ctx.CreateSessionAsync(Client, new SessionConfig { Tools = [AIFunctionFactory.Create(EncryptStringDenied, "encrypt_string")], OnPermissionRequest = async (request, invocation) => PermissionDecision.Reject(), diff --git a/dotnet/test/Harness/E2ETestBackend.cs b/dotnet/test/Harness/E2ETestBackend.cs new file mode 100644 index 0000000000..04808ed7a9 --- /dev/null +++ b/dotnet/test/Harness/E2ETestBackend.cs @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +namespace GitHub.Copilot.Test.Harness; + +internal enum E2ETestBackend +{ + Capi, + AnthropicMessages, + OpenAIResponses, + OpenAICompletions, +} + +internal static class E2ETestBackendConfiguration +{ + internal const string EnvironmentVariable = "COPILOT_SDK_E2E_BACKEND"; + private const string AnthropicDefaultModel = "claude-sonnet-4.5"; + private const string OpenAIDefaultModel = "gpt-4.1"; + private const string FakeCredential = "fake-byok-credential-for-e2e-tests"; + + internal static E2ETestBackend Current + => Parse(Environment.GetEnvironmentVariable(EnvironmentVariable)); + + internal static E2ETestBackend Parse(string? value) + => value?.Trim().ToLowerInvariant() switch + { + null or "" or "capi" => E2ETestBackend.Capi, + "anthropic-messages" => E2ETestBackend.AnthropicMessages, + "openai-responses" => E2ETestBackend.OpenAIResponses, + "openai-completions" => E2ETestBackend.OpenAICompletions, + _ => throw new ArgumentOutOfRangeException( + nameof(value), + value, + $"Unsupported {EnvironmentVariable} value. Expected capi, anthropic-messages, openai-responses, or openai-completions."), + }; + + internal static string ToWireName(this E2ETestBackend backend) + => backend switch + { + E2ETestBackend.Capi => "capi", + E2ETestBackend.AnthropicMessages => "anthropic-messages", + E2ETestBackend.OpenAIResponses => "openai-responses", + E2ETestBackend.OpenAICompletions => "openai-completions", + _ => throw new ArgumentOutOfRangeException(nameof(backend), backend, null), + }; + + internal static void ApplyProvider( + this E2ETestBackend backend, + SessionConfig config, + string proxyUrl) + { + if (backend == E2ETestBackend.Capi + || config.Provider is not null + || config.Providers is not null) + { + return; + } + + var model = config.Model ??= backend.GetDefaultModel(); + config.Provider = CreateProvider(backend, proxyUrl, model); + } + + internal static void ApplyProvider( + this E2ETestBackend backend, + ResumeSessionConfig config, + string proxyUrl) + { + if (backend == E2ETestBackend.Capi || config.Provider is not null) + { + return; + } + + var model = config.Model ??= backend.GetDefaultModel(); + config.Provider = CreateProvider(backend, proxyUrl, model); + } + + private static string GetDefaultModel(this E2ETestBackend backend) + => backend switch + { + E2ETestBackend.AnthropicMessages => AnthropicDefaultModel, + E2ETestBackend.OpenAIResponses or E2ETestBackend.OpenAICompletions => OpenAIDefaultModel, + _ => throw new ArgumentOutOfRangeException(nameof(backend), backend, null), + }; + + private static ProviderConfig CreateProvider( + E2ETestBackend backend, + string proxyUrl, + string model) + => new() + { + BaseUrl = proxyUrl, + Type = backend switch + { + E2ETestBackend.AnthropicMessages => "anthropic", + E2ETestBackend.OpenAIResponses or E2ETestBackend.OpenAICompletions => "openai", + _ => throw new ArgumentOutOfRangeException(nameof(backend), backend, null), + }, + WireApi = backend switch + { + E2ETestBackend.AnthropicMessages => null, + E2ETestBackend.OpenAIResponses => "responses", + E2ETestBackend.OpenAICompletions => "completions", + _ => throw new ArgumentOutOfRangeException(nameof(backend), backend, null), + }, + BearerToken = FakeCredential, + ModelId = model, + WireModel = model, + }; +} + +internal static class E2ETestTraits +{ + // Trait key used by workflow filters to classify backend compatibility. + internal const string Backend = "E2EBackend"; + + // Requires the default CAPI backend and is excluded from BYOK legs. + internal const string CapiOnly = "CapiOnly"; + + // Owns its backend setup and must not inherit the backend selected by the test matrix. + internal const string SelfConfiguredBackend = "SelfConfiguredBackend"; +} diff --git a/dotnet/test/Harness/E2ETestBase.cs b/dotnet/test/Harness/E2ETestBase.cs index ddd1b894bb..a3006389bb 100644 --- a/dotnet/test/Harness/E2ETestBase.cs +++ b/dotnet/test/Harness/E2ETestBase.cs @@ -76,7 +76,7 @@ protected Task CreateSessionAsync(SessionConfig? config = null) { config ??= new SessionConfig(); config.OnPermissionRequest ??= PermissionHandler.ApproveAll; - return Client.CreateSessionAsync(config); + return Ctx.CreateSessionAsync(Client, config); } /// @@ -96,7 +96,7 @@ protected async Task ResumeSessionAsync(string sessionId, Resume { Connection = RuntimeConnection.ForUri($"localhost:{port}", connectionToken: E2ETestFixture.SharedTcpConnectionToken), }); - return await client.ResumeSessionAsync(sessionId, config); + return await Ctx.ResumeSessionAsync(client, sessionId, config); } protected static string GetSystemMessage(ParsedHttpExchange exchange) diff --git a/dotnet/test/Harness/E2ETestContext.cs b/dotnet/test/Harness/E2ETestContext.cs index f4df1749f0..c01f4efcff 100644 --- a/dotnet/test/Harness/E2ETestContext.cs +++ b/dotnet/test/Harness/E2ETestContext.cs @@ -20,13 +20,13 @@ public sealed class E2ETestContext : IAsyncDisposable /// Optional logger injected by tests; applied to all clients created via . public ILogger? Logger { get; set; } - private readonly CapiProxy _proxy; + private readonly ReplayProxy _proxy; private readonly string _repoRoot; private readonly object _clientsLock = new(); private readonly List _persistentClients = []; private readonly List _transientClients = []; - private E2ETestContext(string homeDir, string workDir, string proxyUrl, CapiProxy proxy, string repoRoot) + private E2ETestContext(string homeDir, string workDir, string proxyUrl, ReplayProxy proxy, string repoRoot) { HomeDir = homeDir; WorkDir = workDir; @@ -50,7 +50,7 @@ public static async Task CreateAsync() homeDir = ResolveSymlinks(homeDir); workDir = ResolveSymlinks(workDir); - var proxy = new CapiProxy(); + var proxy = new ReplayProxy(); var proxyUrl = await proxy.StartAsync(); await proxy.SetCopilotUserByTokenAsync(DefaultGitHubToken, new CopilotUserConfig( Login: "e2e-test-user", @@ -163,7 +163,10 @@ public async Task ConfigureForTestAsync(string testFile, [CallerMemberName] stri // to avoid case collisions on case-insensitive filesystems (macOS/Windows) var sanitizedName = Regex.Replace(testName!, @"[^a-zA-Z0-9]", "_").ToLowerInvariant(); var snapshotPath = Path.Combine(_repoRoot, "test", "snapshots", testFile, $"{sanitizedName}.yaml"); - await _proxy.ConfigureAsync(snapshotPath, WorkDir); + await _proxy.ConfigureAsync( + snapshotPath, + WorkDir, + E2ETestBackendConfiguration.Current.ToWireName()); } public Task> GetExchangesAsync() @@ -244,9 +247,17 @@ public CopilotClient CreateClient( { options ??= new CopilotClientOptions(); - options.WorkingDirectory ??= WorkDir; options.Logger ??= Logger; + // Resolve the working directory the worker should run in. Child-process and + // URI transports take it as a per-client option; the in-process transport + // rejects a per-client WorkingDirectory (the native host spawns the worker + // without a cwd parameter), so — mirroring the Node/Rust harnesses — we point + // THIS process's cwd at the desired directory before the worker spawns and + // clear the per-client option. InProcessEnvIsolationAttribute.After restores + // the cwd after the test. + var desiredWorkingDirectory = options.WorkingDirectory ?? WorkDir; + // Tests must supply environment via the 'environment' parameter, which the // harness routes to the right place per transport (the connection for // child-process transports, the host process for in-process). Setting @@ -300,12 +311,24 @@ public CopilotClient CreateClient( { InProcessEnvIsolation.Apply(name, value); } + + // A per-client WorkingDirectory is rejected in-process; instead point this + // process's cwd at the desired directory so the worker inherits it at spawn + // (restored after the test by InProcessEnvIsolationAttribute). + options.WorkingDirectory = null; + InProcessEnvIsolation.SetWorkingDirectory(desiredWorkingDirectory); } else if (options.Connection is ChildProcessRuntimeConnection child) { // Child-process transport: hand the environment to the spawned child // via the connection, where per-client environment is coherent. child.Environment = env; + options.WorkingDirectory = desiredWorkingDirectory; + } + else + { + // URI / existing-runtime transport: per-client WorkingDirectory applies normally. + options.WorkingDirectory = desiredWorkingDirectory; } // Auto-inject auth token unless connecting to an existing runtime via URI. @@ -332,6 +355,25 @@ public CopilotClient CreateClient( return client; } + public Task CreateSessionAsync( + CopilotClient client, + SessionConfig? config = null) + { + config ??= new SessionConfig(); + E2ETestBackendConfiguration.Current.ApplyProvider(config, ProxyUrl); + return client.CreateSessionAsync(config); + } + + public Task ResumeSessionAsync( + CopilotClient client, + string sessionId, + ResumeSessionConfig? config = null) + { + config ??= new ResumeSessionConfig(); + E2ETestBackendConfiguration.Current.ApplyProvider(config, ProxyUrl); + return client.ResumeSessionAsync(sessionId, config); + } + public void UntrackClient(CopilotClient client) { lock (_clientsLock) diff --git a/dotnet/test/Harness/InProcessEnvIsolation.cs b/dotnet/test/Harness/InProcessEnvIsolation.cs index 14b065204f..af06001eda 100644 --- a/dotnet/test/Harness/InProcessEnvIsolation.cs +++ b/dotnet/test/Harness/InProcessEnvIsolation.cs @@ -22,6 +22,11 @@ internal static class InProcessEnvIsolation // Captured at load, before any fixture/test mutates env. private static readonly Dictionary s_ambient = CaptureEnvironment(); + // The process working directory captured at load, restored after each test so an + // in-process test that repoints the cwd (the FFI worker inherits it at spawn) + // can't leak that change into the next test. + private static readonly string s_ambientCwd = Directory.GetCurrentDirectory(); + // Runs at assembly load so the ambient env is snapshotted before the shared // fixture mirrors per-test env onto the process. Justifies suppressing CA2255. #pragma warning disable CA2255 // ModuleInitializer discouraged in libraries; intentional in this test harness. @@ -56,8 +61,21 @@ public static void NeutralizeAmbientCredentials() } } + // Points the process working directory at the given path so the in-process FFI + // worker inherits it at spawn (the native host has no per-client cwd parameter). + // RestoreAmbient() returns the process to its load-time cwd after the test. + public static void SetWorkingDirectory(string path) => + Directory.SetCurrentDirectory(path); + public static void RestoreAmbient() { + // Unconditionally repoint the process cwd at its load-time value. We must + // not read Directory.GetCurrentDirectory() first: an in-process test can + // chdir into a temp work dir that the harness then deletes, so getcwd() + // would throw FileNotFoundException. SetCurrentDirectory to an absolute + // path succeeds regardless of whether the old cwd still exists. + Directory.SetCurrentDirectory(s_ambientCwd); + foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables()) { var name = (string)entry.Key; diff --git a/dotnet/test/Harness/CapiProxy.cs b/dotnet/test/Harness/ReplayProxy.cs similarity index 94% rename from dotnet/test/Harness/CapiProxy.cs rename to dotnet/test/Harness/ReplayProxy.cs index 39c95fa683..895ebccb87 100644 --- a/dotnet/test/Harness/CapiProxy.cs +++ b/dotnet/test/Harness/ReplayProxy.cs @@ -13,7 +13,7 @@ namespace GitHub.Copilot.Test.Harness; -public sealed partial class CapiProxy : IAsyncDisposable +public sealed partial class ReplayProxy : IAsyncDisposable { private Process? _process; private Task? _startupTask; @@ -76,7 +76,7 @@ async Task StartCoreAsync() { var metadata = JsonSerializer.Deserialize( match.Groups["metadata"].Value, - CapiProxyJsonContext.Default.ProxyStartupMetadata); + ReplayProxyJsonContext.Default.ProxyStartupMetadata); ConnectProxyUrl = metadata?.ConnectProxyUrl; CaFilePath = metadata?.CaFilePath; } @@ -150,16 +150,19 @@ public async Task StopAsync(bool skipWritingCache = false) _startupTask = null; } - public async Task ConfigureAsync(string filePath, string workDir) + public async Task ConfigureAsync(string filePath, string workDir, string backend) { var url = await (_startupTask ?? throw new InvalidOperationException("Proxy not started")); using var client = new HttpClient(); - var response = await client.PostAsJsonAsync($"{url}/config", new ConfigureRequest(filePath, workDir), CapiProxyJsonContext.Default.ConfigureRequest); + var response = await client.PostAsJsonAsync( + $"{url}/config", + new ConfigureRequest(filePath, workDir, backend), + ReplayProxyJsonContext.Default.ConfigureRequest); response.EnsureSuccessStatusCode(); } - private record ConfigureRequest(string FilePath, string WorkDir); + private record ConfigureRequest(string FilePath, string WorkDir, string Backend); private record ProxyStartupMetadata(string? ConnectProxyUrl, string? CaFilePath); @@ -168,7 +171,7 @@ public async Task> GetExchangesAsync() var url = await (_startupTask ?? throw new InvalidOperationException("Proxy not started")); using var client = new HttpClient(); - return await client.GetFromJsonAsync($"{url}/exchanges", CapiProxyJsonContext.Default.ListParsedHttpExchange) + return await client.GetFromJsonAsync($"{url}/exchanges", ReplayProxyJsonContext.Default.ListParsedHttpExchange) ?? []; } @@ -178,7 +181,7 @@ public async Task SetCopilotUserByTokenAsync(string token, CopilotUserConfig res using var client = new HttpClient(); var payload = new CopilotUserByTokenRequest(token, response); - var resp = await client.PostAsJsonAsync($"{url}/copilot-user-config", payload, CapiProxyJsonContext.Default.CopilotUserByTokenRequest); + var resp = await client.PostAsJsonAsync($"{url}/copilot-user-config", payload, ReplayProxyJsonContext.Default.CopilotUserByTokenRequest); resp.EnsureSuccessStatusCode(); } @@ -205,7 +208,7 @@ private static string FindRepoRoot() [JsonSerializable(typeof(CopilotUserByTokenRequest))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(ProxyStartupMetadata))] - private partial class CapiProxyJsonContext : JsonSerializerContext; + private partial class ReplayProxyJsonContext : JsonSerializerContext; } public record CopilotUserByTokenRequest(string Token, CopilotUserConfig Response); diff --git a/dotnet/test/Unit/CanvasTests.cs b/dotnet/test/Unit/CanvasTests.cs index 612814d1f3..4993d88f6f 100644 --- a/dotnet/test/Unit/CanvasTests.cs +++ b/dotnet/test/Unit/CanvasTests.cs @@ -166,6 +166,7 @@ public void SessionCanvasOpenedEvent_UpdatesOpenCanvasSnapshots() ExtensionName = "Counter Provider", InstanceId = "counter-1", Title = "Counter", + Icon = "beaker", Status = "ready", Url = "https://example.test/counter", Input = JsonDocument.Parse("""{"seed":1}""").RootElement.Clone(), @@ -200,6 +201,7 @@ public void SessionCanvasOpenedEvent_UpdatesOpenCanvasSnapshots() ExtensionName = "Counter Provider", InstanceId = "counter-1", Title = "Counter Updated", + Icon = "beaker-filled", Status = "reconnected", Url = "https://example.test/counter-updated", Input = JsonDocument.Parse("""{"seed":2}""").RootElement.Clone(), @@ -212,6 +214,7 @@ public void SessionCanvasOpenedEvent_UpdatesOpenCanvasSnapshots() { Assert.Equal("counter-1", canvas.InstanceId); Assert.Equal("Counter Updated", canvas.Title); + Assert.Equal("beaker-filled", canvas.Icon); Assert.Equal("reconnected", canvas.Status); Assert.Equal("https://example.test/counter-updated", canvas.Url); Assert.Equal(2, canvas.Input!.Value.GetProperty("seed").GetInt32()); @@ -313,6 +316,28 @@ public void ExtensionInfo_Serializes_SourceAndName() Assert.Equal("demo", doc.RootElement.GetProperty("name").GetString()); } + [Fact] + public void CanvasProviderIdentity_Serializes_IdAndName() + { + var options = GetSerializerOptions(); + var identity = new CanvasProviderIdentity { Id = "app:builtin:window-1", Name = "Built-in" }; + var json = JsonSerializer.Serialize(identity, options); + using var doc = JsonDocument.Parse(json); + Assert.Equal("app:builtin:window-1", doc.RootElement.GetProperty("id").GetString()); + Assert.Equal("Built-in", doc.RootElement.GetProperty("name").GetString()); + } + + [Fact] + public void CanvasProviderIdentity_OmitsNullName() + { + var options = GetSerializerOptions(); + var identity = new CanvasProviderIdentity { Id = "app:builtin:window-1" }; + var json = JsonSerializer.Serialize(identity, options); + using var doc = JsonDocument.Parse(json); + Assert.Equal("app:builtin:window-1", doc.RootElement.GetProperty("id").GetString()); + Assert.False(doc.RootElement.TryGetProperty("name", out _)); + } + [Fact] public async Task CanvasHandlerBase_DefaultOnClose_Completes() { @@ -348,6 +373,7 @@ public void SessionConfig_Clone_CopiesCanvasFields() RequestCanvasRenderer = true, RequestExtensions = true, ExtensionInfo = new ExtensionInfo { Source = "github-app", Name = "demo" }, + CanvasProvider = new CanvasProviderIdentity { Id = "app:builtin:window-1", Name = "Built-in" }, CanvasHandler = handler }; @@ -360,6 +386,8 @@ public void SessionConfig_Clone_CopiesCanvasFields() Assert.True(clone.RequestExtensions); Assert.NotNull(clone.ExtensionInfo); Assert.Equal("github-app", clone.ExtensionInfo!.Source); + Assert.NotNull(clone.CanvasProvider); + Assert.Equal("app:builtin:window-1", clone.CanvasProvider!.Id); Assert.Same(handler, clone.CanvasHandler); // Mutating the clone's list does not affect the original. @@ -376,6 +404,7 @@ public void ResumeSessionConfig_Clone_CopiesCanvasFields() Canvases = new[] { new CanvasDeclaration { Id = "c1", DisplayName = "C", Description = "d" } }, RequestCanvasRenderer = true, ExtensionInfo = new ExtensionInfo { Source = "s", Name = "n" }, + CanvasProvider = new CanvasProviderIdentity { Id = "app:builtin:window-2" }, CanvasHandler = handler }; @@ -385,6 +414,8 @@ public void ResumeSessionConfig_Clone_CopiesCanvasFields() Assert.Single(clone.Canvases!); Assert.True(clone.RequestCanvasRenderer); Assert.NotNull(clone.ExtensionInfo); + Assert.NotNull(clone.CanvasProvider); + Assert.Equal("app:builtin:window-2", clone.CanvasProvider!.Id); Assert.Same(handler, clone.CanvasHandler); } diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index f736e8dee4..e1143db17c 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -204,6 +204,57 @@ public async Task ResumeSessionAsync_Throws_When_Same_Client_Already_Tracks_Sess AssertSessionCount(client, sessions: 1); } + [Fact] + public async Task CreateSessionAsync_Serializes_CustomAgent_ReasoningEffort() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await client.StartAsync(); + + await using var session = await client.CreateSessionAsync(new SessionConfig + { + CustomAgents = + [ + new CustomAgentConfig + { + Name = "reasoning-agent", + Prompt = "Think carefully.", + ReasoningEffort = "high" + } + ], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var request = Assert.Single(server.Requests, request => request.Method == "session.create"); + var agent = Assert.Single(request.Params.GetProperty("customAgents").EnumerateArray()); + Assert.Equal("high", agent.GetProperty("reasoningEffort").GetString()); + } + + [Fact] + public async Task CreateSessionAsync_Omits_CustomAgent_ReasoningEffort_When_Unset() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await client.StartAsync(); + + await using var session = await client.CreateSessionAsync(new SessionConfig + { + CustomAgents = + [ + new CustomAgentConfig + { + Name = "default-agent", + Prompt = "Use runtime defaults." + } + ], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var request = Assert.Single(server.Requests, request => request.Method == "session.create"); + var agent = Assert.Single(request.Params.GetProperty("customAgents").EnumerateArray()); + Assert.False(agent.TryGetProperty("reasoningEffort", out _)); + } + [Fact] public async Task CreateSessionAsync_Registers_McpAuth_Interest_Only_When_Handler_Configured() { diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs index 425b580a1b..ec509ab169 100644 --- a/dotnet/test/Unit/CloneTests.cs +++ b/dotnet/test/Unit/CloneTests.cs @@ -82,7 +82,7 @@ public void SessionConfig_Clone_CopiesAllProperties() IncludeSubAgentStreamingEvents = false, McpServers = new Dictionary { ["server1"] = new McpStdioServerConfig { Command = "echo" } }, McpOAuthTokenStorage = McpOAuthTokenStorageMode.Persistent, - CustomAgents = [new CustomAgentConfig { Name = "agent1", Model = "claude-haiku-4.5" }], + CustomAgents = [new CustomAgentConfig { Name = "agent1", Model = "claude-haiku-4.5", ReasoningEffort = "high" }], Agent = "agent1", Capi = new CapiSessionOptions { EnableWebSocketResponses = false }, Cloud = new CloudSessionOptions @@ -128,6 +128,7 @@ public void SessionConfig_Clone_CopiesAllProperties() Assert.Equal(original.McpOAuthTokenStorage, clone.McpOAuthTokenStorage); Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count); Assert.Equal(original.CustomAgents[0].Model, clone.CustomAgents[0].Model); + Assert.Equal(original.CustomAgents[0].ReasoningEffort, clone.CustomAgents[0].ReasoningEffort); Assert.Equal(original.Agent, clone.Agent); Assert.Same(original.Capi, clone.Capi); Assert.Same(original.Cloud, clone.Cloud); diff --git a/dotnet/test/Unit/CopilotToolTests.cs b/dotnet/test/Unit/CopilotToolTests.cs index 9c9e2a93ba..c3f5861492 100644 --- a/dotnet/test/Unit/CopilotToolTests.cs +++ b/dotnet/test/Unit/CopilotToolTests.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.AI; using System.ComponentModel; using System.Text.Json; +using System.Text.Json.Nodes; using Xunit; namespace GitHub.Copilot.Test.Unit; @@ -43,6 +44,34 @@ public void DefineTool_Omits_Copilot_Metadata_When_Flags_Are_False() Assert.False(function.AdditionalProperties.ContainsKey("defer")); } + [Fact] + public void DefineTool_Sets_Metadata_In_Additional_Properties() + { + var metadata = new Dictionary + { + ["github.com/copilot:safeForTelemetry"] = new JsonObject + { + ["name"] = true, + ["inputsNames"] = false + } + }; + + var function = CopilotTool.DefineTool( + ReturnsOk, + new CopilotToolOptions { Metadata = metadata }); + + Assert.True(function.AdditionalProperties.TryGetValue("metadata", out var value)); + Assert.Same(metadata, value); + } + + [Fact] + public void DefineTool_Omits_Metadata_When_Unset() + { + var function = CopilotTool.DefineTool(ReturnsOk); + + Assert.False(function.AdditionalProperties.ContainsKey("metadata")); + } + [Fact] public void DefineTool_Accepts_Lambda_Handlers_Without_Casts() { diff --git a/dotnet/test/Unit/E2ETestBackendTests.cs b/dotnet/test/Unit/E2ETestBackendTests.cs new file mode 100644 index 0000000000..f7c39a5083 --- /dev/null +++ b/dotnet/test/Unit/E2ETestBackendTests.cs @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using GitHub.Copilot.Test.Harness; +using Xunit; + +namespace GitHub.Copilot.Test.Unit; + +public class E2ETestBackendTests +{ + [Theory] + [InlineData(null, "capi")] + [InlineData("", "capi")] + [InlineData("capi", "capi")] + [InlineData("ANTHROPIC-MESSAGES", "anthropic-messages")] + [InlineData("openai-responses", "openai-responses")] + [InlineData("openai-completions", "openai-completions")] + public void ParsesBackend(string? value, string expected) + => Assert.Equal(expected, E2ETestBackendConfiguration.Parse(value).ToWireName()); + + [Fact] + public void RejectsUnknownBackend() + => Assert.Throws( + () => E2ETestBackendConfiguration.Parse("unknown")); + + [Theory] + [InlineData("anthropic-messages", "anthropic", null, "claude-sonnet-4.5")] + [InlineData("openai-responses", "openai", "responses", "gpt-4.1")] + [InlineData("openai-completions", "openai", "completions", "gpt-4.1")] + public void AppliesProvider( + string backendValue, + string expectedType, + string? expectedWireApi, + string expectedModel) + { + var backend = E2ETestBackendConfiguration.Parse(backendValue); + var config = new SessionConfig(); + backend.ApplyProvider(config, "http://localhost:1234"); + + Assert.Equal(expectedModel, config.Model); + Assert.Equal("http://localhost:1234", config.Provider!.BaseUrl); + Assert.Equal(expectedType, config.Provider.Type); + Assert.Equal(expectedWireApi, config.Provider.WireApi); + Assert.Equal(expectedModel, config.Provider.ModelId); + Assert.Equal(expectedModel, config.Provider.WireModel); + Assert.False(string.IsNullOrEmpty(config.Provider.BearerToken)); + } + + [Fact] + public void PreservesExplicitModel() + { + var config = new SessionConfig { Model = "test-model" }; + E2ETestBackend.OpenAIResponses.ApplyProvider(config, "http://localhost:1234"); + + Assert.Equal("test-model", config.Model); + Assert.Equal("test-model", config.Provider!.ModelId); + Assert.Equal("test-model", config.Provider.WireModel); + } + + [Fact] + public void PreservesExplicitProvider() + { + var provider = new ProviderConfig + { + Type = "custom", + ModelId = "provider-model", + }; + var config = new SessionConfig + { + Model = "session-model", + Provider = provider, + }; + + E2ETestBackend.OpenAIResponses.ApplyProvider(config, "http://localhost:1234"); + + Assert.Equal("session-model", config.Model); + Assert.Same(provider, config.Provider); + } +} diff --git a/dotnet/test/Unit/ForwardCompatibilityTests.cs b/dotnet/test/Unit/ForwardCompatibilityTests.cs index 09133dfb52..b52a7713f7 100644 --- a/dotnet/test/Unit/ForwardCompatibilityTests.cs +++ b/dotnet/test/Unit/ForwardCompatibilityTests.cs @@ -168,6 +168,24 @@ public void FromJson_UnknownEventType_WithUnknownEnumInData_DoesNotThrow() Assert.Equal("unknown", result.Type); } + [Fact] + public void FromJson_InternalEventType_ReturnsBaseSessionEvent() + { + var json = """ + { + "id": "12345678-1234-1234-1234-123456789abc", + "timestamp": "2026-06-15T10:30:00Z", + "type": "session.memory_changed", + "data": {} + } + """; + + var result = SessionEvent.FromJson(json); + + Assert.IsType(result); + Assert.Equal("unknown", result.Type); + } + [Fact] public void FromJson_KnownEventType_WithUnknownEnumInData_PreservesValue() { diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index f6fa31d88c..ae7e885138 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -856,6 +856,48 @@ public void HooksInvokeResponse_SerializesNullOutput_AsEmptyOrNoOutputProperty() // else: property omitted, which is fine (runtime treats undefined output as no-op) } + [Fact] + public void ToolResultObject_SerializesToolReferences_WithSdkOptions() + { + var options = GetSerializerOptions(); + var original = new ToolResultObject + { + TextResultForLlm = "found 2 tools", + ResultType = "success", + ToolReferences = ["get_weather", "check_status"], + }; + + var json = JsonSerializer.Serialize(original, options); + using var document = JsonDocument.Parse(json); + var root = document.RootElement; + Assert.Equal("found 2 tools", root.GetProperty("textResultForLlm").GetString()); + var refs = root.GetProperty("toolReferences"); + Assert.Equal(JsonValueKind.Array, refs.ValueKind); + Assert.Equal(2, refs.GetArrayLength()); + Assert.Equal("get_weather", refs[0].GetString()); + Assert.Equal("check_status", refs[1].GetString()); + + var deserialized = JsonSerializer.Deserialize(json, options); + Assert.NotNull(deserialized); + string[] expectedReferences = ["get_weather", "check_status"]; + Assert.Equal(expectedReferences, deserialized!.ToolReferences); + } + + [Fact] + public void ToolResultObject_OmitsToolReferences_WhenNull_WithSdkOptions() + { + var options = GetSerializerOptions(); + var original = new ToolResultObject + { + TextResultForLlm = "ok", + ResultType = "success", + }; + + var json = JsonSerializer.Serialize(original, options); + using var document = JsonDocument.Parse(json); + Assert.False(document.RootElement.TryGetProperty("toolReferences", out _)); + } + private static JsonSerializerOptions GetSerializerOptions() { var prop = typeof(CopilotClient) diff --git a/go/README.md b/go/README.md index 5787e5a53e..44ba01d66a 100644 --- a/go/README.md +++ b/go/README.md @@ -101,6 +101,49 @@ Follow these steps to embed the CLI: That's it! When your application calls `copilot.NewClient` without a `Connection` field (or with an empty `StdioConnection{}`) and no `COPILOT_CLI_PATH` environment variable, the SDK will automatically install the embedded CLI to a cache directory and use it for all operations. +The bundler prepares the native runtime library required by the [in-process transport](#in-process-transport-experimental). It is included in the application only when building with the `copilot_inprocess` build tag. + +## In-process transport (Experimental) + +> **Experimental:** the in-process API may change in a future release. + +By default the SDK starts the runtime as a child process and talks JSON-RPC over stdio or TCP. The **in-process** transport instead loads a native runtime library directly into your process. + +Build your application with the `copilot_inprocess` build tag: + +```sh +go build -tags copilot_inprocess +``` + +```go +client := copilot.NewClient(&copilot.ClientOptions{ + Connection: copilot.InProcessConnection{}, +}) +if err := client.Start(context.Background()); err != nil { + log.Fatal(err) +} +defer client.Stop() +``` + +Resolution and requirements: + +- The application must be built with the `copilot_inprocess` build tag. +- Set `COPILOT_SDK_DEFAULT_CONNECTION=inprocess` to select the in-process + transport when `ClientOptions.Connection` is nil. An explicit connection + always takes precedence. +- Set `COPILOT_CLI_PATH` only when using an externally provisioned compatible runtime package; otherwise the bundled runtime is used. No `PATH` lookup is performed. +- Embedded runtime versions are isolated in separate cache directories. Start fails loudly if the native runtime is unavailable. +- Linux in-process bundles include both glibc and musl runtime packages and select the matching package automatically at startup. +- Only one native runtime version may be loaded per process. + +The in-process transport rejects options that cannot be honored by a runtime hosted in your shared process (each panics at `NewClient`): + +- `Env` — the host process has a single environment block. Set variables on the host process environment instead. +- `WorkingDirectory` — the runtime shares the host process's working directory. Change the process working directory before creating the client. +- `Telemetry` — per-client telemetry is lowered to native-runtime environment variables. Use a child-process transport for per-client telemetry. + +Implemented with pure-Go FFI (via [purego](https://github.com/ebitengine/purego)), so `CGO_ENABLED=0` and cross-compilation are preserved; no C toolchain is required. + ## API Reference ### Client @@ -142,11 +185,14 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec **ClientOptions:** - `Connection` (RuntimeConnection): How the SDK connects to the runtime. Construct via one of: - - `StdioConnection{Path, Args}` — spawn a runtime over stdio (the default if `Connection` is nil) - - `TCPConnection{Port, ConnectionToken, Path, Args}` — spawn a runtime that listens on TCP + - `StdioConnection{Path, Args, Env}` — spawn a runtime over stdio (the default if `Connection` is nil) + - `TCPConnection{Port, ConnectionToken, Path, Args, Env}` — spawn a runtime that listens on TCP - `URIConnection{URL, ConnectionToken}` — connect to an already-running runtime (no process spawned) + - `InProcessConnection{}` — **Experimental.** Host the runtime in-process via the native FFI library instead of spawning a child process. See [In-process transport](#in-process-transport-experimental) below. When `Path` is empty for stdio/tcp, the SDK uses the bundled CLI (or `COPILOT_CLI_PATH` env var). + + `StdioConnection` and `TCPConnection` accept an optional connection-level `Env`. Set environment variables via **either** the client-level `Env` option or the connection's `Env`, not both (setting both panics); prefer the connection-level `Env`. - `WorkingDirectory` (string): Working directory for the runtime process - `BaseDirectory` (string): Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned runtime. When empty, the runtime defaults to `~/.copilot`. Ignored with `URIConnection`. This does **not** affect where the Go SDK extracts the embedded CLI binary; use `embeddedcli.Config.Dir` for the extraction/cache location. - `LogLevel` (string): Log level. When empty (default), the runtime uses its own default level (the SDK does not pass `--log-level`). diff --git a/go/canvas.go b/go/canvas.go index 375f7c6eef..e31598bb1e 100644 --- a/go/canvas.go +++ b/go/canvas.go @@ -42,6 +42,24 @@ type ExtensionInfo struct { Name string `json:"name"` } +// CanvasProviderIdentity is the stable identity for a host/SDK connection +// that supplies built-in canvases. +// +// When set on session create or resume, the runtime uses ID verbatim as the +// agent-facing canvas extension id, so host-provided canvases survive +// reconnect and CLI restart. +// +// Experimental: CanvasProviderIdentity is part of an experimental +// wire-protocol surface and may change or be removed in future SDK or CLI +// releases. +type CanvasProviderIdentity struct { + // ID is an opaque, stable provider id used verbatim as the canvas + // extension id. + ID string `json:"id"` + // Name is an optional display name surfaced as the canvas extension name. + Name *string `json:"name,omitempty"` +} + // CanvasError is a structured error returned from canvas handlers. // // Wire envelope: diff --git a/go/client.go b/go/client.go index 5b8dabf8f2..f243268aa4 100644 --- a/go/client.go +++ b/go/client.go @@ -93,6 +93,37 @@ func validateSessionFSConfig(config *SessionFSConfig) error { return nil } +// validateEnvironmentOptions enforces the transport-specific rules for +// per-client environment, working directory, and telemetry. It panics (fails +// loud) on a misconfiguration, matching the other SDKs. +// +// The in-process transport loads the native runtime into this process, whose +// single environment block and process-global working directory cannot carry +// per-client values, and whose telemetry lowers to shared process-global env +// vars — so options that depend on them are rejected there. Child-process +// transports each own their OS process, so per-connection env is allowed, but +// setting it in both the client-level option and the connection is rejected. +func validateEnvironmentOptions(connection RuntimeConnection, opts *ClientOptions) { + if _, ok := connection.(InProcessConnection); ok { + if opts.Env != nil { + panic("Env is not supported with InProcessConnection: the in-process transport loads the native runtime into the shared host process, whose single environment block cannot carry per-client values. Set the variables on the host process environment instead.") + } + if opts.WorkingDirectory != "" { + panic("WorkingDirectory is not supported with InProcessConnection: the native runtime shares the host process working directory. Use a child-process transport, or set the process working directory before creating the client.") + } + if opts.Telemetry != nil { + panic("Telemetry is not supported with InProcessConnection: telemetry configuration is lowered to environment variables read by native runtime code running in the shared host process, so per-client telemetry cannot be honored in-process. Configure telemetry via the host process environment, or use a child-process transport.") + } + return + } + + if cp, ok := connection.(childProcessConnection); ok { + if cp.connEnv() != nil && opts.Env != nil { + panic("Set environment variables via either the client-level Env option or the connection's Env, not both. Prefer the connection-level Env for child-process transports.") + } + } +} + // Client manages the connection to the Copilot CLI server and provides session management. // // The Client can either spawn a CLI server process or connect to an existing server. @@ -124,6 +155,8 @@ type Client struct { isExternalServer bool conn net.Conn // stores net.Conn for external TCP connections useStdio bool // resolved value from options + useInProcess bool // true for InProcessConnection (FFI transport) + ffiHost inProcessHost // resolved process options for the spawned runtime (zero values for URIConnection) cliPath string cliArgs []string @@ -193,10 +226,15 @@ func NewClient(options *ClientOptions) *Client { opts = *options } - // Resolve the connection. nil defaults to an empty StdioConnection. + // Resolve the connection. An explicit connection always wins; otherwise + // honor the same process/environment override as the other SDKs. connection := opts.Connection if connection == nil { - connection = StdioConnection{} + env := opts.Env + if env == nil { + env = os.Environ() + } + connection = resolveDefaultConnection(env) } switch conn := connection.(type) { case StdioConnection: @@ -223,32 +261,52 @@ func NewClient(options *ClientOptions) *Client { client.isExternalServer = true client.useStdio = false client.tcpConnectionToken = conn.ConnectionToken + case InProcessConnection: + client.useStdio = false + client.useInProcess = true default: panic(fmt.Sprintf("unknown RuntimeConnection type: %T", connection)) } + // Validate transport-specific option constraints (fail loud). The in-process + // transport loads the runtime into this process, whose single environment + // block, process-global working directory, and shared telemetry state cannot + // carry per-client values. Child-process transports may set env via either + // the client-level option or the connection, but not both. + validateEnvironmentOptions(connection, &opts) + // Validate auth options when connecting to an external runtime. if client.isExternalServer && (opts.GitHubToken != "" || opts.UseLoggedInUser != nil) { panic("GitHubToken and UseLoggedInUser cannot be used with URIConnection (external runtime manages its own auth)") } + // For child-process transports, a connection-level env takes precedence over + // the client-level env (setting both was rejected above). Resolve it before + // defaulting so an explicit empty connection env stays authoritative. + if cp, ok := connection.(childProcessConnection); ok { + if env := cp.connEnv(); env != nil { + opts.Env = env + } + } + // Default Env to current environment if not set if opts.Env == nil { opts.Env = os.Environ() } - // Check effective environment for CLI path (only if not explicitly set via options) - if client.cliPath == "" { + // Check the effective environment for a child-process runtime override. + if client.cliPath == "" && !client.useInProcess { if cliPath := getEnvValue(opts.Env, "COPILOT_CLI_PATH"); cliPath != "" { client.cliPath = cliPath } } // Resolve the effective connection token: explicit value if set; else if the SDK - // spawns its own runtime in TCP mode, generate a UUID; otherwise empty. + // spawns its own runtime in TCP mode, generate a UUID; otherwise empty. The + // in-process transport uses no socket, so it needs no connection token. if client.tcpConnectionToken != "" { client.effectiveConnectionToken = client.tcpConnectionToken - } else if !client.useStdio && !client.isExternalServer { + } else if !client.useStdio && !client.isExternalServer && !client.useInProcess { client.effectiveConnectionToken = uuid.NewString() } @@ -266,6 +324,27 @@ func NewClient(options *ClientOptions) *Client { return client } +const defaultConnectionEnvVar = "COPILOT_SDK_DEFAULT_CONNECTION" + +// resolveDefaultConnection selects the transport when no explicit connection +// was supplied. The override is primarily used by hosts and the E2E transport +// matrix; explicit connection options always take precedence. +func resolveDefaultConnection(env []string) RuntimeConnection { + value := getEnvValue(env, defaultConnectionEnvVar) + switch { + case value == "", strings.EqualFold(value, "stdio"): + return StdioConnection{} + case strings.EqualFold(value, "inprocess"): + return InProcessConnection{} + default: + panic(fmt.Sprintf( + "invalid %s value %q: expected \"inprocess\", \"stdio\", or unset", + defaultConnectionEnvVar, + value, + )) + } +} + // getEnvValue looks up a key in an environment slice ([]string of "KEY=VALUE"). // Returns the value if found, or empty string otherwise. func getEnvValue(env []string, key string) string { @@ -451,7 +530,7 @@ func (c *Client) Stop() error { c.startStopMux.Lock() defer c.startStopMux.Unlock() - if c.process != nil && !c.isExternalServer && c.RPC != nil { + if (c.process != nil || c.ffiHost != nil) && !c.isExternalServer && c.RPC != nil { rpcClient := c.RPC runtimeShutdownStart := time.Now() shutdownDone := make(chan error, 1) @@ -486,6 +565,13 @@ func (c *Client) Stop() error { } c.process = nil + // Tear down the in-process FFI host (closes the connection and shuts down the + // native runtime). No child process to reap in this mode. + if c.ffiHost != nil { + c.ffiHost.Dispose() + c.ffiHost = nil + } + // Close external TCP connection if exists if c.isExternalServer && c.conn != nil { if err := c.conn.Close(); err != nil { @@ -567,6 +653,12 @@ func (c *Client) ForceStop() { } c.process = nil + // Dispose the in-process FFI host (if any) without waiting on graceful shutdown. + if c.ffiHost != nil { + c.ffiHost.Dispose() + c.ffiHost = nil + } + // Close external TCP connection if exists if c.isExternalServer && c.conn != nil { _ = c.conn.Close() // Ignore errors @@ -722,16 +814,20 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.DisabledSkills = config.DisabledSkills req.InfiniteSessions = config.InfiniteSessions req.LargeOutput = config.LargeOutput + req.ToolSearch = config.ToolSearch req.Memory = config.Memory req.GitHubToken = config.GitHubToken req.RemoteSession = config.RemoteSession req.Cloud = config.Cloud req.Canvases = config.Canvases + req.ExtensionInfo = config.ExtensionInfo + req.CanvasProvider = config.CanvasProvider req.RequestCanvasRenderer = config.RequestCanvasRenderer req.RequestExtensions = config.RequestExtensions req.ExtensionSDKPath = config.ExtensionSDKPath req.ExtensionInfo = config.ExtensionInfo req.ExpAssignments = config.ExpAssignments + req.EnableManagedSettings = config.EnableManagedSettings if len(config.Commands) > 0 { cmds := make([]wireCommand, 0, len(config.Commands)) @@ -1085,16 +1181,20 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.DisabledSkills = config.DisabledSkills req.InfiniteSessions = config.InfiniteSessions req.LargeOutput = config.LargeOutput + req.ToolSearch = config.ToolSearch req.Memory = config.Memory req.GitHubToken = config.GitHubToken req.RemoteSession = config.RemoteSession req.Canvases = config.Canvases req.OpenCanvases = config.OpenCanvases + req.ExtensionInfo = config.ExtensionInfo + req.CanvasProvider = config.CanvasProvider req.RequestCanvasRenderer = config.RequestCanvasRenderer req.RequestExtensions = config.RequestExtensions req.ExtensionSDKPath = config.ExtensionSDKPath req.ExtensionInfo = config.ExtensionInfo req.ExpAssignments = config.ExpAssignments + req.EnableManagedSettings = config.EnableManagedSettings if config.OnPermissionRequest != nil { req.RequestPermission = Bool(true) } @@ -1745,6 +1845,10 @@ const stderrBufferSize = 64 * 1024 // This spawns the CLI server as a subprocess using the configured transport // mode (stdio or TCP). func (c *Client) startCLIServer(ctx context.Context) error { + if c.useInProcess { + return c.startInProcess(ctx) + } + cliPath := c.cliPath if cliPath == "" { // If no CLI path is provided, attempt to use the embedded CLI if available @@ -1954,7 +2058,122 @@ func (c *Client) startCLIServer(ctx context.Context) error { } } +// startInProcess loads the native runtime library and wires the JSON-RPC client +// to its FFI byte streams. +func (c *Client) startInProcess(ctx context.Context) error { + if !inProcessAvailable { + return errors.New("in-process transport unavailable: rebuild with -tags copilot_inprocess on a supported platform") + } + + runtimePath := c.cliPath + if runtimePath == "" { + // The in-process transport does not resolve a bare command name from PATH + // (unlike the child-process transport). + if p := getEnvValue(c.options.Env, "COPILOT_CLI_PATH"); p != "" { + runtimePath = p + } + } + if runtimePath == "" { + runtimePath = embeddedcli.Path() + } + if runtimePath == "" { + return errors.New("in-process runtime unavailable: set COPILOT_CLI_PATH to a compatible runtime package or build with the bundled embedded runtime") + } + + config := c.inProcessHostConfig() + + host, err := createInProcessHost(runtimePath, config) + if err != nil { + return err + } + // Own the host before the blocking handshake so a cancelled or failed start + // leaves it disposable by Stop/ForceStop rather than leaking (host.Start runs + // on its own goroutine and cannot be interrupted once the native call begins). + c.ffiHost = host + + errCh := make(chan error, 1) + go func() { errCh <- host.Start() }() + select { + case err := <-errCh: + if err != nil { + host.Dispose() + c.ffiHost = nil + return err + } + case <-ctx.Done(): + c.ffiHost = nil + go func() { + <-errCh + host.Dispose() + }() + return ctx.Err() + } + + c.client = jsonrpc2.NewClient(host.Writer(), host.Reader()) + c.client.SetOnClose(func() { + // Run in a goroutine to avoid deadlocking with Stop/ForceStop, which hold + // startStopMux while waiting for readLoop to finish. + go func() { + c.startStopMux.Lock() + defer c.startStopMux.Unlock() + c.state = stateDisconnected + }() + }) + c.RPC = rpc.NewServerRPC(c.client) + c.internalRPC = rpc.NewInternalServerRPC(c.client) + c.setupNotificationHandler() + c.client.Start() + return nil +} + +func (c *Client) inProcessHostConfig() inProcessHostConfig { + args := make([]string, 0, 8) + if c.options.LogLevel != "" { + args = append(args, "--log-level", c.options.LogLevel) + } + if c.options.GitHubToken != "" { + args = append(args, "--auth-token-env", "COPILOT_SDK_AUTH_TOKEN") + } + useLoggedInUser := true + if c.options.UseLoggedInUser != nil { + useLoggedInUser = *c.options.UseLoggedInUser + } else if c.options.GitHubToken != "" { + useLoggedInUser = false + } + if !useLoggedInUser { + args = append(args, "--no-auto-login") + } + if c.options.SessionIdleTimeoutSeconds > 0 { + args = append(args, "--session-idle-timeout", strconv.Itoa(c.options.SessionIdleTimeoutSeconds)) + } + if c.options.EnableRemoteSessions { + args = append(args, "--remote") + } + + environment := make(map[string]string) + if c.options.GitHubToken != "" { + environment["COPILOT_SDK_AUTH_TOKEN"] = c.options.GitHubToken + } + if c.options.BaseDirectory != "" { + environment["COPILOT_HOME"] = c.options.BaseDirectory + } + if c.options.Mode == ModeEmpty { + environment["COPILOT_DISABLE_KEYTAR"] = "1" + } + + return inProcessHostConfig{ + Environment: environment, + Args: args, + } +} + func (c *Client) killProcess() error { + // Tear down the in-process FFI host on error paths that reuse killProcess to + // abort a start (there is no OS process to kill in that mode). + if c.ffiHost != nil { + c.ffiHost.Dispose() + c.ffiHost = nil + } if p := c.osProcess.Swap(nil); p != nil { if err := p.Kill(); err != nil { return fmt.Errorf("failed to kill CLI process: %w", err) @@ -2016,8 +2235,8 @@ func (c *Client) monitorProcess() { // connectToServer establishes a connection to the server. func (c *Client) connectToServer(ctx context.Context) error { - if c.useStdio { - // Already connected via stdio in startCLIServer + if c.useStdio || c.useInProcess { + // Already connected: stdio in startCLIServer, FFI streams in startInProcess. return nil } @@ -2071,7 +2290,6 @@ func (c *Client) setupNotificationHandler() { c.client.SetRequestHandler("userInput.request", jsonrpc2.RequestHandlerFor(c.handleUserInputRequest)) c.client.SetRequestHandler("exitPlanMode.request", jsonrpc2.RequestHandlerFor(c.handleExitPlanModeRequest)) c.client.SetRequestHandler("autoModeSwitch.request", jsonrpc2.RequestHandlerFor(c.handleAutoModeSwitchRequest)) - c.client.SetRequestHandler("hooks.invoke", jsonrpc2.RequestHandlerFor(c.handleHooksInvoke)) c.client.SetRequestHandler("systemMessage.transform", jsonrpc2.RequestHandlerFor(c.handleSystemMessageTransform)) rpc.RegisterClientSessionAPIHandlers(c.client, func(sessionID string) *rpc.ClientSessionAPIHandlers { c.sessionsMux.Lock() @@ -2082,21 +2300,25 @@ func (c *Client) setupNotificationHandler() { } return session.clientSessionAPIs }) - if c.options.RequestHandler != nil || c.options.OnGitHubTelemetry != nil { - handlers := &rpc.ClientGlobalAPIHandlers{} - if c.options.RequestHandler != nil { - handlers.LlmInference = newCopilotRequestAdapter(c.options.RequestHandler, func() *rpc.ServerLlmInferenceAPI { - if c.RPC == nil { - return nil - } - return c.RPC.LlmInference - }) - } - if c.options.OnGitHubTelemetry != nil { - handlers.GitHubTelemetry = &gitHubTelemetryAdapter{callback: c.options.OnGitHubTelemetry} - } - rpc.RegisterClientGlobalAPIHandlers(c.client, handlers) + // hooks.invoke is a client-global RPC method: one connection-level handler + // receives every hook callback and routes to the owning session via the + // payload's sessionId. Always register the global handlers so the generated + // hooks.invoke handler is wired to our dispatcher. + handlers := &rpc.ClientGlobalAPIHandlers{ + Hooks: &hooksAdapter{client: c}, + } + if c.options.RequestHandler != nil { + handlers.LlmInference = newCopilotRequestAdapter(c.options.RequestHandler, func() *rpc.ServerLlmInferenceAPI { + if c.RPC == nil { + return nil + } + return c.RPC.LlmInference + }) + } + if c.options.OnGitHubTelemetry != nil { + handlers.GitHubTelemetry = &gitHubTelemetryAdapter{callback: c.options.OnGitHubTelemetry} } + rpc.RegisterClientGlobalAPIHandlers(c.client, handlers) } // gitHubTelemetryAdapter adapts the OnGitHubTelemetry option to the generated @@ -2204,7 +2426,8 @@ func (c *Client) handleAutoModeSwitchRequest(req autoModeSwitchRequest) (*autoMo return &autoModeSwitchResponse{Response: response}, nil } -// handleHooksInvoke handles a hooks invocation from the CLI server. +// handleHooksInvoke routes a hook callback to its owning session, keyed by the +// payload's sessionId. func (c *Client) handleHooksInvoke(req hooksInvokeRequest) (map[string]any, *jsonrpc2.Error) { if req.SessionID == "" || req.Type == "" { return nil, &jsonrpc2.Error{Code: -32602, Message: "invalid hooks invoke payload"} @@ -2229,6 +2452,34 @@ func (c *Client) handleHooksInvoke(req hooksInvokeRequest) (map[string]any, *jso return result, nil } +// hooksAdapter implements the generated rpc.HooksHandler, delegating to the +// client's per-session hook dispatcher. +type hooksAdapter struct { + client *Client +} + +func (a *hooksAdapter) Invoke(request *rpc.HookInvokeRequest) (*rpc.HookInvokeResponse, error) { + rawInput, err := json.Marshal(request.Input) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("invalid hooks invoke payload: %v", err)} + } + + result, rpcErr := a.client.handleHooksInvoke(hooksInvokeRequest{ + SessionID: request.SessionID, + Type: string(request.HookType), + Input: rawInput, + }) + if rpcErr != nil { + return nil, rpcErr + } + + response := &rpc.HookInvokeResponse{} + if result != nil { + response.Output = result["output"] + } + return response, nil +} + // handleSystemMessageTransform handles a system message transform request from the CLI server. func (c *Client) handleSystemMessageTransform(req systemMessageTransformRequest) (systemMessageTransformResponse, *jsonrpc2.Error) { if req.SessionID == "" { diff --git a/go/client_test.go b/go/client_test.go index bd48baacd7..b24628d836 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -251,6 +251,82 @@ func TestClient_ForwardsCapiOptionsToSessionRequests(t *testing.T) { assertCapiEnableWebSocketResponses(t, <-resumeParams) } +func TestClient_ForwardsCanvasProviderToSessionRequests(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 + }) + + _, err := client.CreateSession(t.Context(), &SessionConfig{ + ExtensionInfo: &ExtensionInfo{Source: "github-app", Name: "counter-provider"}, + CanvasProvider: &CanvasProviderIdentity{ID: "app:builtin:window-1", Name: String("Built-in")}, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + assertCanvasProviderForwarded(t, <-createParams, "app:builtin:window-1", "Built-in", "counter-provider") + + 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-canvas","workspacePath":"/workspace"}`), nil + }) + + _, err = client.ResumeSessionWithOptions(t.Context(), "resumed-canvas", &ResumeSessionConfig{ + CanvasProvider: &CanvasProviderIdentity{ID: "app:builtin:window-1"}, + }) + if err != nil { + t.Fatalf("ResumeSessionWithOptions failed: %v", err) + } + assertCanvasProviderForwarded(t, <-resumeParams, "app:builtin:window-1", "", "") +} + +// assertCanvasProviderForwarded checks the outbound params carry canvasProvider +// with the expected id. A non-empty wantName asserts the name is present; an +// empty wantName asserts the name key is omitted from the wire. A non-empty +// wantExtensionName asserts extensionInfo.name is forwarded alongside it. +func assertCanvasProviderForwarded(t *testing.T, params json.RawMessage, wantID, wantName, wantExtensionName string) { + t.Helper() + + var decoded map[string]any + if err := json.Unmarshal(params, &decoded); err != nil { + t.Fatalf("failed to unmarshal request params: %v", err) + } + provider, ok := decoded["canvasProvider"].(map[string]any) + if !ok { + t.Fatalf("expected canvasProvider object in request params, got %T", decoded["canvasProvider"]) + } + if provider["id"] != wantID { + t.Fatalf("expected canvasProvider.id=%q, got %v", wantID, provider["id"]) + } + if wantName == "" { + if _, present := provider["name"]; present { + t.Fatalf("expected canvasProvider.name to be omitted, got %v", provider["name"]) + } + } else if provider["name"] != wantName { + t.Fatalf("expected canvasProvider.name=%q, got %v", wantName, provider["name"]) + } + if wantExtensionName != "" { + info, ok := decoded["extensionInfo"].(map[string]any) + if !ok { + t.Fatalf("expected extensionInfo object in request params, got %T", decoded["extensionInfo"]) + } + if info["name"] != wantExtensionName { + t.Fatalf("expected extensionInfo.name=%q, got %v", wantExtensionName, info["name"]) + } + } +} + func TestClient_ForwardsNewSessionOptionsToSessionRequests(t *testing.T) { rpcClient, server, _ := newRuntimeShutdownRpcPair(t) t.Cleanup(server.Stop) @@ -549,6 +625,198 @@ func TestClient_EnvOptions(t *testing.T) { }) } +func TestClient_InProcessConnection(t *testing.T) { + t.Run("requires build tag", func(t *testing.T) { + if inProcessAvailable { + t.Skip("in-process transport is enabled") + } + + client := NewClient(&ClientOptions{Connection: InProcessConnection{}}) + err := client.Start(context.Background()) + if err == nil || !strings.Contains(err.Error(), "-tags copilot_inprocess") { + t.Fatalf("Expected build-tag error, got %v", err) + } + }) + + t.Run("uses in-process transport", func(t *testing.T) { + client := NewClient(&ClientOptions{Connection: InProcessConnection{}}) + if !client.useInProcess { + t.Error("Expected useInProcess=true for InProcessConnection") + } + if client.useStdio { + t.Error("Expected useStdio=false for InProcessConnection") + } + if client.isExternalServer { + t.Error("Expected isExternalServer=false for InProcessConnection") + } + if client.cliPath != "" { + t.Errorf("Expected in-process cliPath to stay empty at construction, got %q", client.cliPath) + } + }) + + t.Run("does not resolve COPILOT_CLI_PATH into cliPath at construction", func(t *testing.T) { + t.Setenv("COPILOT_CLI_PATH", "/from/env/copilot") + client := NewClient(&ClientOptions{Connection: InProcessConnection{}}) + if client.cliPath != "" { + t.Errorf("Expected in-process cliPath to stay empty at construction, got %q", client.cliPath) + } + }) + + t.Run("panics when Env is set", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic when Env is set with InProcessConnection") + } + }() + NewClient(&ClientOptions{ + Connection: InProcessConnection{}, + Env: []string{"FOO=bar"}, + }) + }) + + t.Run("panics when WorkingDirectory is set", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic when WorkingDirectory is set with InProcessConnection") + } + }() + NewClient(&ClientOptions{ + Connection: InProcessConnection{}, + WorkingDirectory: "/tmp/work", + }) + }) + + t.Run("panics when Telemetry is set", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic when Telemetry is set with InProcessConnection") + } + }() + NewClient(&ClientOptions{ + Connection: InProcessConnection{}, + Telemetry: &TelemetryConfig{ExporterType: "file"}, + }) + }) + + t.Run("forwards typed runtime options", func(t *testing.T) { + client := NewClient(&ClientOptions{ + Connection: InProcessConnection{}, + GitHubToken: "test-token", + UseLoggedInUser: Bool(false), + BaseDirectory: "/copilot-home", + LogLevel: "debug", + SessionIdleTimeoutSeconds: 30, + EnableRemoteSessions: true, + Mode: ModeEmpty, + }) + + config := client.inProcessHostConfig() + expectedArgs := []string{ + "--log-level", "debug", + "--auth-token-env", "COPILOT_SDK_AUTH_TOKEN", + "--no-auto-login", + "--session-idle-timeout", "30", + "--remote", + } + if !reflect.DeepEqual(config.Args, expectedArgs) { + t.Fatalf("Expected managed arguments %v, got %v", expectedArgs, config.Args) + } + expectedEnvironment := map[string]string{ + "COPILOT_SDK_AUTH_TOKEN": "test-token", + "COPILOT_HOME": "/copilot-home", + "COPILOT_DISABLE_KEYTAR": "1", + } + if !reflect.DeepEqual(config.Environment, expectedEnvironment) { + t.Fatalf("Expected managed environment %v, got %v", expectedEnvironment, config.Environment) + } + }) +} + +func TestClient_DefaultConnection(t *testing.T) { + t.Run("defaults to stdio when override is unset", func(t *testing.T) { + t.Setenv(defaultConnectionEnvVar, "") + + client := NewClient(nil) + + if !client.useStdio || client.useInProcess { + t.Fatalf("Expected stdio default, got useStdio=%v useInProcess=%v", client.useStdio, client.useInProcess) + } + }) + + t.Run("selects in-process case-insensitively", func(t *testing.T) { + t.Setenv(defaultConnectionEnvVar, "InPrOcEsS") + + client := NewClient(nil) + + if !client.useInProcess || client.useStdio { + t.Fatalf("Expected in-process default, got useStdio=%v useInProcess=%v", client.useStdio, client.useInProcess) + } + }) + + t.Run("accepts explicit stdio override", func(t *testing.T) { + t.Setenv(defaultConnectionEnvVar, "STDIO") + + client := NewClient(nil) + + if !client.useStdio || client.useInProcess { + t.Fatalf("Expected stdio default, got useStdio=%v useInProcess=%v", client.useStdio, client.useInProcess) + } + }) + + t.Run("explicit connection takes precedence", func(t *testing.T) { + t.Setenv(defaultConnectionEnvVar, "inprocess") + + client := NewClient(&ClientOptions{Connection: TCPConnection{Port: 1234}}) + + if client.useInProcess || client.useStdio || client.port != 1234 { + t.Fatalf("Expected explicit TCP connection to win, got useStdio=%v useInProcess=%v port=%d", client.useStdio, client.useInProcess, client.port) + } + }) + + t.Run("panics for invalid override", func(t *testing.T) { + t.Setenv(defaultConnectionEnvVar, "tcp") + + defer func() { + if r := recover(); r == nil { + t.Fatal("Expected invalid default connection override to panic") + } + }() + NewClient(nil) + }) +} + +func TestClient_ConnectionLevelEnv(t *testing.T) { + t.Run("rejects env set on both client and connection", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic when env is set on both client and connection") + } + }() + NewClient(&ClientOptions{ + Connection: StdioConnection{Env: []string{"A=1"}}, + Env: []string{"B=2"}, + }) + }) + + t.Run("stdio connection env is used when client env is unset", func(t *testing.T) { + client := NewClient(&ClientOptions{ + Connection: StdioConnection{Env: []string{"ONLY=conn"}}, + }) + if len(client.options.Env) != 1 || client.options.Env[0] != "ONLY=conn" { + t.Errorf("Expected connection-level Env to be used, got %v", client.options.Env) + } + }) + + t.Run("tcp connection env is used when client env is unset", func(t *testing.T) { + client := NewClient(&ClientOptions{ + Connection: TCPConnection{Port: 9000, Env: []string{"ONLY=conn"}}, + }) + if len(client.options.Env) != 1 || client.options.Env[0] != "ONLY=conn" { + t.Errorf("Expected connection-level Env to be used, got %v", client.options.Env) + } + }) +} + func TestClient_SessionIdleTimeoutSeconds(t *testing.T) { t.Run("should store SessionIdleTimeoutSeconds option", func(t *testing.T) { client := NewClient(&ClientOptions{ @@ -1160,6 +1428,53 @@ func TestToolDefer(t *testing.T) { }) } +func TestToolMetadata(t *testing.T) { + t.Run("Metadata is serialized in tool definition", func(t *testing.T) { + tool := Tool{ + Name: "my_tool", + Description: "A custom tool", + Metadata: map[string]any{ + "github.com/copilot:safeForTelemetry": map[string]any{"name": true, "inputsNames": false}, + }, + Handler: func(_ ToolInvocation) (ToolResult, error) { return ToolResult{}, nil }, + } + data, err := json.Marshal(tool) + if err != nil { + t.Fatalf("failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("failed to unmarshal: %v", err) + } + meta, ok := m["metadata"].(map[string]any) + if !ok { + t.Fatalf("expected metadata object, got %v", m) + } + if _, ok := meta["github.com/copilot:safeForTelemetry"]; !ok { + t.Errorf("expected namespaced key preserved, got %v", meta) + } + }) + + t.Run("Metadata omitted when unset", func(t *testing.T) { + tool := Tool{ + Name: "custom_tool", + Description: "A custom tool", + Handler: func(_ ToolInvocation) (ToolResult, error) { return ToolResult{}, nil }, + } + data, err := json.Marshal(tool) + if err != nil { + t.Fatalf("failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("failed to unmarshal: %v", err) + } + if _, ok := m["metadata"]; ok { + t.Errorf("expected metadata to be omitted, got %v", m) + } + }) +} + func TestClient_CreateSession_AllowsMissingPermissionHandler(t *testing.T) { t.Run("accepts nil config before connection validation", func(t *testing.T) { client := NewClient(&ClientOptions{Connection: StdioConnection{Path: "/__nonexistent_copilot_binary__"}}) diff --git a/go/cmd/bundler/main.go b/go/cmd/bundler/main.go index 1e5f5ecd8b..e63d1fde66 100644 --- a/go/cmd/bundler/main.go +++ b/go/cmd/bundler/main.go @@ -91,14 +91,47 @@ func main() { fmt.Printf("Building bundle for %s (CLI version %s)\n", *platform, version) - binaryPath, sha256Hash, err := buildBundle(info, version, outputPath) + binaryPath, sha256Hash, runtimeArtifactPath, runtimeHash, err := buildBundle(info, version, outputPath, goos) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } + var muslBinaryPath, muslRuntimeArtifactPath string + var muslBinaryHash, muslRuntimeHash []byte + if goos == "linux" { + muslInfo := platformInfo{ + npmPlatform: strings.Replace(info.npmPlatform, "linux-", "linuxmusl-", 1), + binaryName: info.binaryName, + } + muslOutputPath := filepath.Join(*output, defaultOutputFileName(version, "linuxmusl", goarch, info.binaryName)) + muslBinaryPath, muslBinaryHash, muslRuntimeArtifactPath, muslRuntimeHash, err = buildBundle( + muslInfo, + version, + muslOutputPath, + goos, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + } + // Generate the Go file with embed directive - if err := generateGoFile(goos, goarch, binaryPath, version, sha256Hash, "main"); err != nil { + if err := generateGoFile( + goos, + goarch, + binaryPath, + version, + sha256Hash, + runtimeArtifactPath, + runtimeHash, + muslBinaryPath, + muslBinaryHash, + muslRuntimeArtifactPath, + muslRuntimeHash, + "main", + ); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } @@ -253,12 +286,16 @@ func isHex(s string) bool { return true } -// buildBundle downloads the CLI binary and writes it to outputPath. -func buildBundle(info platformInfo, cliVersion, outputPath string) (string, []byte, error) { +// buildBundle downloads the CLI binary (and, when the CLI package ships it, the +// native in-process runtime library) and writes them to outputPath's directory. +// It returns the CLI bundle path and hash, plus the runtime-library artifact path +// and hash (both empty when the package does not ship the runtime library). +func buildBundle(info platformInfo, cliVersion, outputPath, goos string) (string, []byte, string, []byte, error) { outputDir := filepath.Dir(outputPath) if outputDir == "" { outputDir = "." } + runtimeArtifactPath := filepath.Join(outputDir, runtimeLibArtifactName(cliVersion, info.npmPlatform, goos)) // Check if output already exists if _, err := os.Stat(outputPath); err == nil { @@ -266,68 +303,254 @@ func buildBundle(info platformInfo, cliVersion, outputPath string) (string, []by fmt.Printf("Output %s already exists, skipping download\n", outputPath) sha256Hash, err := sha256FileFromCompressed(outputPath) if err != nil { - return "", nil, fmt.Errorf("failed to hash existing output: %w", err) + return "", nil, "", nil, fmt.Errorf("failed to hash existing output: %w", err) } if err := downloadCLILicense(cliVersion, outputPath); err != nil { - return "", nil, fmt.Errorf("failed to download CLI license: %w", err) + return "", nil, "", nil, fmt.Errorf("failed to download CLI license: %w", err) + } + // Reuse an existing runtime-library artifact if present. + if _, err := os.Stat(runtimeArtifactPath); err == nil { + runtimeHash, err := sha256FileFromCompressed(runtimeArtifactPath) + if err != nil { + return "", nil, "", nil, fmt.Errorf("failed to hash existing runtime library: %w", err) + } + return outputPath, sha256Hash, runtimeArtifactPath, runtimeHash, nil } - return outputPath, sha256Hash, nil + return outputPath, sha256Hash, "", nil, nil } // Create temp directory for download tempDir, err := os.MkdirTemp("", "copilot-bundler-*") if err != nil { - return "", nil, fmt.Errorf("failed to create temp dir: %w", err) + return "", nil, "", nil, fmt.Errorf("failed to create temp dir: %w", err) } defer os.RemoveAll(tempDir) // Download the binary - binaryPath, err := downloadCLIBinary(info.npmPlatform, info.binaryName, cliVersion, tempDir) + binaryPath, tarballPath, err := downloadCLIBinary(info.npmPlatform, info.binaryName, cliVersion, tempDir) if err != nil { - return "", nil, fmt.Errorf("failed to download CLI binary: %w", err) + return "", nil, "", nil, fmt.Errorf("failed to download CLI binary: %w", err) } // Create output directory if needed if outputDir != "." { if err := os.MkdirAll(outputDir, 0755); err != nil { - return "", nil, fmt.Errorf("failed to create output directory: %w", err) + return "", nil, "", nil, fmt.Errorf("failed to create output directory: %w", err) } } sha256Hash, err := sha256File(binaryPath) if err != nil { - return "", nil, fmt.Errorf("failed to hash output binary: %w", err) + return "", nil, "", nil, fmt.Errorf("failed to hash output binary: %w", err) } if err := compressZstdFile(binaryPath, outputPath); err != nil { - return "", nil, fmt.Errorf("failed to write output binary: %w", err) + return "", nil, "", nil, fmt.Errorf("failed to write output binary: %w", err) } if err := downloadCLILicense(cliVersion, outputPath); err != nil { - return "", nil, fmt.Errorf("failed to download CLI license: %w", err) + return "", nil, "", nil, fmt.Errorf("failed to download CLI license: %w", err) } + + // Extract the native in-process runtime library from the same tarball, if the + // package ships it (older CLI versions do not). Missing is not an error — the + // generated file simply omits the runtime embed for that platform. + rawLibPath := filepath.Join(tempDir, "runtime.node") + found, err := extractOptionalFileFromTarball(tarballPath, tempDir, + "package/prebuilds/"+info.npmPlatform+"/runtime.node", "runtime.node") + if err != nil { + return "", nil, "", nil, fmt.Errorf("failed to extract runtime library: %w", err) + } + var runtimeHash []byte + returnedRuntimeArtifact := "" + if found { + runtimeHash, err = sha256File(rawLibPath) + if err != nil { + return "", nil, "", nil, fmt.Errorf("failed to hash runtime library: %w", err) + } + if err := compressZstdFile(rawLibPath, runtimeArtifactPath); err != nil { + return "", nil, "", nil, fmt.Errorf("failed to write runtime library: %w", err) + } + returnedRuntimeArtifact = runtimeArtifactPath + fmt.Printf("Successfully created %s\n", runtimeArtifactPath) + } else { + fmt.Printf("Package %s does not ship a runtime library; in-process transport unavailable for this platform bundle\n", info.npmPlatform) + } + fmt.Printf("Successfully created %s\n", outputPath) - return outputPath, sha256Hash, nil + return outputPath, sha256Hash, returnedRuntimeArtifact, runtimeHash, nil } -// generateGoFile creates a Go source file that embeds the binary and metadata. -func generateGoFile(goos, goarch, binaryPath, cliVersion string, sha256Hash []byte, pkgName string) error { - // Generate Go file path: zcopilot_linux_amd64.go (without version) +// 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)) +} + +// runtimeLibExt returns the shared-library extension for the target OS. +func runtimeLibExt(goos string) string { + switch goos { + case "windows": + return "dll" + case "darwin": + return "dylib" + default: + return "so" + } +} + +// generateGoFile creates separate source files for normal and in-process builds. +// Both embed the CLI, while only the copilot_inprocess-tagged file embeds the +// native runtime library. +func generateGoFile( + goos, + goarch, + binaryPath, + cliVersion string, + sha256Hash []byte, + runtimeArtifactPath string, + runtimeHash []byte, + muslBinaryPath string, + muslBinaryHash []byte, + muslRuntimeArtifactPath string, + muslRuntimeHash []byte, + pkgName string, +) error { binaryName := filepath.Base(binaryPath) licenseName := licenseFileName(binaryName) - goFileName := fmt.Sprintf("zcopilot_%s_%s.go", goos, goarch) - goFilePath := filepath.Join(filepath.Dir(binaryPath), goFileName) hashBase64 := "" if len(sha256Hash) > 0 { hashBase64 = base64.StdEncoding.EncodeToString(sha256Hash) } - content := fmt.Sprintf(`// Code generated by copilot-sdk bundler; DO NOT EDIT. + outputDir := filepath.Dir(binaryPath) + defaultPath := filepath.Join(outputDir, fmt.Sprintf("zcopilot_%s_%s.go", goos, goarch)) + defaultContent := generatedGoFileContent( + "!copilot_inprocess", + pkgName, + binaryName, + licenseName, + cliVersion, + hashBase64, + "", + nil, + "", + nil, + "", + nil, + ) + if err := os.WriteFile(defaultPath, []byte(defaultContent), 0644); err != nil { + return err + } + + inProcessPath := filepath.Join(outputDir, fmt.Sprintf("zcopilot_inprocess_%s_%s.go", goos, goarch)) + inProcessContent := generatedGoFileContent( + "copilot_inprocess", + pkgName, + binaryName, + licenseName, + cliVersion, + hashBase64, + runtimeArtifactPath, + runtimeHash, + muslBinaryPath, + muslBinaryHash, + muslRuntimeArtifactPath, + muslRuntimeHash, + ) + if err := os.WriteFile(inProcessPath, []byte(inProcessContent), 0644); err != nil { + return err + } + + fmt.Printf("Generated %s\n", defaultPath) + fmt.Printf("Generated %s\n", inProcessPath) + return nil +} + +func generatedGoFileContent( + buildConstraint, + pkgName, + binaryName, + licenseName, + cliVersion, + hashBase64, + runtimeArtifactPath string, + runtimeHash []byte, + muslBinaryPath string, + muslBinaryHash []byte, + muslRuntimeArtifactPath string, + muslRuntimeHash []byte, +) string { + runtimeEmbed := "" + runtimeConfig := "" + runtimeReader := "" + if runtimeArtifactPath != "" { + runtimeArtifactName := filepath.Base(runtimeArtifactPath) + runtimeHashBase64 := base64.StdEncoding.EncodeToString(runtimeHash) + runtimeEmbed = fmt.Sprintf(` +//go:embed %s +var localEmbeddedCopilotRuntimeLib []byte +`, runtimeArtifactName) + runtimeConfig = fmt.Sprintf(` + RuntimeLib: runtimeLibReader(), + RuntimeLibHash: mustDecodeBase64(%q),`, runtimeHashBase64) + runtimeReader = ` +func runtimeLibReader() io.Reader { + r, err := zstd.NewReader(bytes.NewReader(localEmbeddedCopilotRuntimeLib)) + if err != nil { + panic("failed to create zstd reader: " + err.Error()) + } + return r +} +` + } + + muslEmbed := "" + muslConfig := "" + muslReaders := "" + if muslBinaryPath != "" && muslRuntimeArtifactPath != "" { + muslBinaryName := filepath.Base(muslBinaryPath) + muslBinaryHashBase64 := base64.StdEncoding.EncodeToString(muslBinaryHash) + muslRuntimeName := filepath.Base(muslRuntimeArtifactPath) + muslRuntimeHashBase64 := base64.StdEncoding.EncodeToString(muslRuntimeHash) + muslEmbed = fmt.Sprintf(` +//go:embed %s +var localEmbeddedCopilotCLILinuxMusl []byte + +//go:embed %s +var localEmbeddedCopilotRuntimeLibLinuxMusl []byte +`, muslBinaryName, muslRuntimeName) + muslConfig = fmt.Sprintf(` + LinuxMuslCli: linuxMuslCLIReader(), + LinuxMuslCliHash: mustDecodeBase64(%q), + LinuxMuslRuntimeLib: linuxMuslRuntimeLibReader(), + LinuxMuslRuntimeLibHash: mustDecodeBase64(%q),`, muslBinaryHashBase64, muslRuntimeHashBase64) + muslReaders = ` +func linuxMuslCLIReader() io.Reader { + r, err := zstd.NewReader(bytes.NewReader(localEmbeddedCopilotCLILinuxMusl)) + if err != nil { + panic("failed to create zstd reader: " + err.Error()) + } + return r +} + +func linuxMuslRuntimeLibReader() io.Reader { + r, err := zstd.NewReader(bytes.NewReader(localEmbeddedCopilotRuntimeLibLinuxMusl)) + if err != nil { + panic("failed to create zstd reader: " + err.Error()) + } + return r +} +` + } + + return fmt.Sprintf(`//go:build %s + +// Code generated by copilot-sdk bundler; DO NOT EDIT. package %s import ( "bytes" - "io" "encoding/base64" _ "embed" + "io" "github.com/github/copilot-sdk/go/embeddedcli" "github.com/klauspost/compress/zstd" @@ -338,14 +561,15 @@ var localEmbeddedCopilotCLI []byte //go:embed %s var localEmbeddedCopilotCLILicense []byte - +%s +%s func init() { embeddedcli.Setup(embeddedcli.Config{ Cli: cliReader(), License: localEmbeddedCopilotCLILicense, Version: %q, - CliHash: mustDecodeBase64(%q), + CliHash: mustDecodeBase64(%q),%s%s }) } @@ -356,7 +580,8 @@ func cliReader() io.Reader { } return r } - +%s +%s func mustDecodeBase64(s string) []byte { b, err := base64.StdEncoding.DecodeString(s) if err != nil { @@ -364,73 +589,68 @@ func mustDecodeBase64(s string) []byte { } return b } -`, pkgName, binaryName, licenseName, cliVersion, hashBase64) - - if err := os.WriteFile(goFilePath, []byte(content), 0644); err != nil { - return err - } - - fmt.Printf("Generated %s\n", goFilePath) - return nil +`, buildConstraint, pkgName, binaryName, licenseName, runtimeEmbed, muslEmbed, cliVersion, hashBase64, runtimeConfig, muslConfig, runtimeReader, muslReaders) } -// downloadCLIBinary downloads the npm tarball and extracts the CLI binary. -func downloadCLIBinary(npmPlatform, binaryName, cliVersion, destDir string) (string, error) { +// downloadCLIBinary downloads the npm tarball 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) fmt.Printf("Downloading from %s...\n", tarballURL) resp, err := http.Get(tarballURL) if err != nil { - return "", fmt.Errorf("failed to download: %w", err) + return "", "", fmt.Errorf("failed to download: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to download: %s", resp.Status) + return "", "", fmt.Errorf("failed to download: %s", resp.Status) } // Save tarball to temp file tarballPath := filepath.Join(destDir, fmt.Sprintf("copilot-%s-%s.tgz", npmPlatform, cliVersion)) tarballFile, err := os.Create(tarballPath) if err != nil { - return "", fmt.Errorf("failed to create tarball file: %w", err) + return "", "", fmt.Errorf("failed to create tarball file: %w", err) } if _, err := io.Copy(tarballFile, resp.Body); err != nil { tarballFile.Close() - return "", fmt.Errorf("failed to save tarball: %w", err) + 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) + return "", "", fmt.Errorf("failed to close tarball file: %w", err) } // Extract only the CLI binary to avoid unpacking the full package tree. binaryPath := filepath.Join(destDir, binaryName) if err := extractFileFromTarball(tarballPath, destDir, "package/"+binaryName, binaryName); err != nil { - return "", fmt.Errorf("failed to extract binary: %w", err) + return "", "", fmt.Errorf("failed to extract binary: %w", err) } // Verify binary exists if _, err := os.Stat(binaryPath); err != nil { - return "", fmt.Errorf("binary not found after extraction: %w", err) + return "", "", fmt.Errorf("binary not found after extraction: %w", err) } // Make executable on Unix if !strings.HasSuffix(binaryName, ".exe") { if err := os.Chmod(binaryPath, 0755); err != nil { - return "", fmt.Errorf("failed to chmod binary: %w", err) + return "", "", fmt.Errorf("failed to chmod binary: %w", err) } } stat, err := os.Stat(binaryPath) if err != nil { - return "", fmt.Errorf("failed to stat binary: %w", err) + return "", "", fmt.Errorf("failed to stat binary: %w", err) } sizeMB := float64(stat.Size()) / 1024 / 1024 fmt.Printf("Downloaded %s (%.1f MB)\n", binaryName, sizeMB) - return binaryPath, nil + return binaryPath, tarballPath, nil } // downloadCLILicense downloads the @github/copilot package and writes its license next to outputPath. @@ -561,6 +781,21 @@ func extractFileFromTarball(tarballPath, destDir, targetPath, outputName string) return fmt.Errorf("file %q not found in tarball", targetPath) } +// extractOptionalFileFromTarball extracts a single file from a .tgz into destDir +// like extractFileFromTarball, but returns (false, nil) instead of an error when +// the file is absent. Used for the runtime library, which older CLI packages do +// not ship. +func extractOptionalFileFromTarball(tarballPath, destDir, targetPath, outputName string) (bool, error) { + err := extractFileFromTarball(tarballPath, destDir, targetPath, outputName) + if err == nil { + return true, nil + } + if strings.Contains(err.Error(), "not found in tarball") { + return false, nil + } + return false, err +} + // compressZstdFile compresses src into dst using zstd. func compressZstdFile(src, dst string) error { srcFile, err := os.Open(src) diff --git a/go/cmd/bundler/main_test.go b/go/cmd/bundler/main_test.go new file mode 100644 index 0000000000..badc791359 --- /dev/null +++ b/go/cmd/bundler/main_test.go @@ -0,0 +1,81 @@ +package main + +import ( + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestGenerateGoFileGatesRuntimeEmbed(t *testing.T) { + dir := t.TempDir() + binaryPath := filepath.Join(dir, "copilot.zst") + runtimePath := filepath.Join(dir, "runtime.node.zst") + muslBinaryPath := filepath.Join(dir, "copilot-musl.zst") + muslRuntimePath := filepath.Join(dir, "runtime-musl.node.zst") + for _, path := range []string{ + binaryPath, + licensePathForOutput(binaryPath), + runtimePath, + muslBinaryPath, + muslRuntimePath, + } { + if err := os.WriteFile(path, []byte("test"), 0644); err != nil { + t.Fatal(err) + } + } + + hash := make([]byte, 32) + if err := generateGoFile( + "linux", + "amd64", + binaryPath, + "1.2.3", + hash, + runtimePath, + hash, + muslBinaryPath, + hash, + muslRuntimePath, + hash, + "main", + ); err != nil { + t.Fatal(err) + } + + defaultSource, err := os.ReadFile(filepath.Join(dir, "zcopilot_linux_amd64.go")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(defaultSource), "//go:build !copilot_inprocess") { + t.Fatal("default embed file does not exclude copilot_inprocess builds") + } + if strings.Contains(string(defaultSource), "localEmbeddedCopilotRuntimeLib") { + t.Fatal("default embed file includes the native runtime") + } + if _, err := parser.ParseFile(token.NewFileSet(), "zcopilot_linux_amd64.go", defaultSource, parser.AllErrors); err != nil { + t.Fatalf("default generated source is invalid: %v", err) + } + + inProcessSource, err := os.ReadFile(filepath.Join(dir, "zcopilot_inprocess_linux_amd64.go")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(inProcessSource), "//go:build copilot_inprocess") { + t.Fatal("in-process embed file does not require the copilot_inprocess tag") + } + if !strings.Contains(string(inProcessSource), "localEmbeddedCopilotRuntimeLib") { + t.Fatal("in-process embed file does not include the native runtime") + } + if !strings.Contains(string(inProcessSource), "localEmbeddedCopilotCLILinuxMusl") { + t.Fatal("in-process embed file does not include the Linux musl CLI") + } + if !strings.Contains(string(inProcessSource), "localEmbeddedCopilotRuntimeLibLinuxMusl") { + t.Fatal("in-process embed file does not include the Linux musl runtime") + } + if _, err := parser.ParseFile(token.NewFileSet(), "zcopilot_inprocess_linux_amd64.go", inProcessSource, parser.AllErrors); err != nil { + t.Fatalf("in-process generated source is invalid: %v", err) + } +} diff --git a/go/copilot_request_handler.go b/go/copilot_request_handler.go index c1ac52bc2e..ba8bb9b919 100644 --- a/go/copilot_request_handler.go +++ b/go/copilot_request_handler.go @@ -50,8 +50,11 @@ var sharedHTTPTransport = func() http.RoundTripper { // CopilotRequestContext is the per-request context handed to every // [CopilotRequestHandler] seam. type CopilotRequestContext struct { - RequestID string - SessionID string + RequestID string + SessionID string + AgentID string + ParentAgentID string + InteractionType string // Transport is "http" (covering plain HTTP and SSE) or "websocket". Transport string Method string @@ -144,12 +147,12 @@ type CopilotWebSocketHandler interface { // copilotContextKey is used to attach [CopilotRequestContext] to an // [http.Request] so custom [http.RoundTripper] implementations can access -// metadata (e.g. SessionID) without additional parameters. +// metadata (e.g. SessionID and AgentID) without additional parameters. type copilotContextKey struct{} // RequestContextFrom returns the [CopilotRequestContext] attached to an // http.Request by the adapter, or nil if not present. Call this from a custom -// [http.RoundTripper] to access metadata such as SessionID. +// [http.RoundTripper] to access metadata such as SessionID and AgentID. func RequestContextFrom(r *http.Request) *CopilotRequestContext { v, _ := r.Context().Value(copilotContextKey{}).(*CopilotRequestContext) return v @@ -194,7 +197,7 @@ func buildHTTPRequest(rctx *CopilotRequestContext) (*http.Request, error) { return nil, err } // Attach rctx so custom RoundTripper implementations can read metadata - // (e.g. SessionID) via [RequestContextFrom]. + // (e.g. SessionID and AgentID) via [RequestContextFrom]. httpReq = httpReq.WithContext(context.WithValue(httpReq.Context(), copilotContextKey{}, rctx)) for name, values := range rctx.Headers { if isForbiddenRequestHeader(name) { @@ -615,14 +618,17 @@ func (a *copilotRequestAdapter) HttpRequestStart(params *rpc.LlmInferenceHTTPReq } rctx := &CopilotRequestContext{ - RequestID: params.RequestID, - SessionID: sessionID, - Method: params.Method, - URL: params.URL, - Headers: headers, - Transport: transport, - body: bodyCh, - Context: ctx, + RequestID: params.RequestID, + SessionID: sessionID, + AgentID: stringOrEmpty(params.AgentID), + ParentAgentID: stringOrEmpty(params.ParentAgentID), + InteractionType: stringOrEmpty(params.InteractionType), + Method: params.Method, + URL: params.URL, + Headers: headers, + Transport: transport, + body: bodyCh, + Context: ctx, } sink := &responseSink{requestID: params.RequestID, adapter: a, exchange: exchange} go a.runHandler(rctx, sink, exchange) @@ -706,6 +712,13 @@ func (a *copilotRequestAdapter) removePending(requestID string) { a.mu.Unlock() } +func stringOrEmpty(value *string) string { + if value == nil { + return "" + } + return *value +} + func decodeChunkData(data string, binary bool) ([]byte, error) { if binary { return base64.StdEncoding.DecodeString(data) diff --git a/go/definetool.go b/go/definetool.go index bc223dc10d..a63aeab9f8 100644 --- a/go/definetool.go +++ b/go/definetool.go @@ -207,7 +207,7 @@ func generateSchemaForType(t reflect.Type) map[string]any { } // Handle pointer types - if t.Kind() == reflect.Ptr { + if t.Kind() == reflect.Pointer { t = t.Elem() } diff --git a/go/embeddedcli/installer.go b/go/embeddedcli/installer.go index 6edddf281a..9702b3aec6 100644 --- a/go/embeddedcli/installer.go +++ b/go/embeddedcli/installer.go @@ -5,9 +5,10 @@ import "github.com/github/copilot-sdk/go/internal/embeddedcli" // Config defines the inputs used to install and locate the embedded Copilot CLI. // // Cli and CliHash are required. If Dir is empty, the CLI is installed into the -// system cache directory. Version is used to suffix the installed binary name to -// allow multiple versions to coexist. License, when provided, is written next -// to the installed binary. +// system cache directory. When Version is set, the CLI and runtime library are +// installed into a version-specific child directory so multiple versions can +// coexist. Linux musl alternatives, when provided, are selected automatically. +// License, when provided, is written next to the installed binary. type Config = embeddedcli.Config // Setup sets the embedded GitHub Copilot CLI install configuration. diff --git a/go/go.mod b/go/go.mod index 586a5d3360..ba0f4feb73 100644 --- a/go/go.mod +++ b/go/go.mod @@ -9,6 +9,7 @@ require ( require ( github.com/coder/websocket v1.8.15 + github.com/ebitengine/purego v0.10.1 github.com/google/uuid v1.6.0 go.opentelemetry.io/otel v1.35.0 go.opentelemetry.io/otel/trace v1.35.0 diff --git a/go/go.sum b/go/go.sum index e7ac53d5a4..cab5b6aabe 100644 --- a/go/go.sum +++ b/go/go.sum @@ -2,6 +2,8 @@ github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNU github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY= +github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/go/inprocess.go b/go/inprocess.go new file mode 100644 index 0000000000..c74410e42b --- /dev/null +++ b/go/inprocess.go @@ -0,0 +1,15 @@ +package copilot + +import "io" + +type inProcessHost interface { + Start() error + Writer() io.WriteCloser + Reader() io.ReadCloser + Dispose() +} + +type inProcessHostConfig struct { + Environment map[string]string + Args []string +} diff --git a/go/inprocess_disabled.go b/go/inprocess_disabled.go new file mode 100644 index 0000000000..b86ed5ca36 --- /dev/null +++ b/go/inprocess_disabled.go @@ -0,0 +1,11 @@ +//go:build !copilot_inprocess || (!darwin && !linux && !windows) + +package copilot + +import "errors" + +const inProcessAvailable = false + +func createInProcessHost(string, inProcessHostConfig) (inProcessHost, error) { + return nil, errors.New("in-process transport unavailable") +} diff --git a/go/inprocess_enabled.go b/go/inprocess_enabled.go new file mode 100644 index 0000000000..c20013d8ab --- /dev/null +++ b/go/inprocess_enabled.go @@ -0,0 +1,11 @@ +//go:build copilot_inprocess && (darwin || linux || windows) + +package copilot + +import "github.com/github/copilot-sdk/go/internal/ffihost" + +const inProcessAvailable = true + +func createInProcessHost(runtimePath string, config inProcessHostConfig) (inProcessHost, error) { + return ffihost.Create(runtimePath, config.Environment, config.Args) +} diff --git a/go/internal/e2e/byok_bearer_token_provider_e2e_test.go b/go/internal/e2e/byok_bearer_token_provider_e2e_test.go index 2f298596d1..33e32b1322 100644 --- a/go/internal/e2e/byok_bearer_token_provider_e2e_test.go +++ b/go/internal/e2e/byok_bearer_token_provider_e2e_test.go @@ -111,6 +111,7 @@ func (rt *byokCapturingRoundTripper) reset() { // 3. per-provider dispatch routes each provider's turn to its own callback, and // the resulting token reaches that provider's endpoint. func TestBYOKBearerTokenProvider(t *testing.T) { + testharness.SkipIfInProcess(t, "an LLM inference provider is process-global in-process") ctx := testharness.NewTestContext(t) rt := &byokCapturingRoundTripper{} handler := &copilot.CopilotRequestHandler{Transport: rt} diff --git a/go/internal/e2e/copilot_request_cancel_error_e2e_test.go b/go/internal/e2e/copilot_request_cancel_error_e2e_test.go index c48a617021..46091d5ac7 100644 --- a/go/internal/e2e/copilot_request_cancel_error_e2e_test.go +++ b/go/internal/e2e/copilot_request_cancel_error_e2e_test.go @@ -54,6 +54,7 @@ func (tr *throwingTransport) RoundTrip(req *http.Request) (*http.Response, error } func TestCopilotRequestError(t *testing.T) { + testharness.SkipIfInProcess(t, "an LLM inference provider is process-global in-process") ctx := testharness.NewTestContext(t) transport := &throwingTransport{} handler := &copilot.CopilotRequestHandler{Transport: transport} @@ -132,6 +133,7 @@ func waitFor(t *testing.T, predicate func() bool, timeout time.Duration) { } func TestCopilotRequestCancel(t *testing.T) { + testharness.SkipIfInProcess(t, "an LLM inference provider is process-global in-process") ctx := testharness.NewTestContext(t) transport := newCancellingTransport() handler := &copilot.CopilotRequestHandler{Transport: transport} diff --git a/go/internal/e2e/copilot_request_handler_e2e_test.go b/go/internal/e2e/copilot_request_handler_e2e_test.go index 052a1bdebc..a0cfcb63ea 100644 --- a/go/internal/e2e/copilot_request_handler_e2e_test.go +++ b/go/internal/e2e/copilot_request_handler_e2e_test.go @@ -119,6 +119,7 @@ func (rt *rewritingRoundTripper) RoundTrip(req *http.Request) (*http.Response, e } func TestCopilotRequestHandler(t *testing.T) { + testharness.SkipIfInProcess(t, "an LLM inference provider is process-global in-process") ctx := testharness.NewTestContext(t) counters := &handlerCounters{} httpURL, wsURL := startFakeUpstreams(t, counters) 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 e88b91c971..f7673bd457 100644 --- a/go/internal/e2e/copilot_request_session_id_e2e_test.go +++ b/go/internal/e2e/copilot_request_session_id_e2e_test.go @@ -16,9 +16,12 @@ import ( ) type interceptedRequest struct { - url string - sessionID string - body string + url string + sessionID string + agentID string + parentAgentID string + interactionType string + body string } // recordingTransport intercepts every model-layer request, records its URL and @@ -32,8 +35,14 @@ type recordingTransport struct { func (rt *recordingTransport) RoundTrip(req *http.Request) (*http.Response, error) { rctx := copilot.RequestContextFrom(req) sessionID := "" + agentID := "" + parentAgentID := "" + interactionType := "" if rctx != nil { sessionID = rctx.SessionID + agentID = rctx.AgentID + parentAgentID = rctx.ParentAgentID + interactionType = rctx.InteractionType } bodyBytes := []byte(nil) if req.Body != nil { @@ -42,7 +51,14 @@ func (rt *recordingTransport) RoundTrip(req *http.Request) (*http.Response, erro bodyText := string(bodyBytes) rt.mu.Lock() - rt.records = append(rt.records, interceptedRequest{url: req.URL.String(), sessionID: sessionID, body: bodyText}) + rt.records = append(rt.records, interceptedRequest{ + url: req.URL.String(), + sessionID: sessionID, + agentID: agentID, + parentAgentID: parentAgentID, + interactionType: interactionType, + body: bodyText, + }) rt.mu.Unlock() if isInferenceURL(req.URL.String()) { @@ -63,7 +79,18 @@ func (rt *recordingTransport) inferenceRecords() []interceptedRequest { return out } +func assertAgentMetadata(t *testing.T, r interceptedRequest) { + t.Helper() + if r.agentID == "" { + t.Fatal("inference request must carry an agent id") + } + if r.interactionType == "" { + t.Fatal("inference request must carry an interaction type") + } +} + func TestCopilotRequestSessionID(t *testing.T) { + testharness.SkipIfInProcess(t, "an LLM inference provider is process-global in-process") ctx := testharness.NewTestContext(t) transport := &recordingTransport{} handler := &copilot.CopilotRequestHandler{Transport: transport} @@ -99,6 +126,7 @@ func TestCopilotRequestSessionID(t *testing.T) { if r.sessionID != capiSessionID { t.Fatalf("CAPI inference request must carry session id %q, got %q", capiSessionID, r.sessionID) } + assertAgentMetadata(t, r) } // Validate the final assistant response arrived (guards against truncated captures) @@ -140,6 +168,7 @@ func TestCopilotRequestSessionID(t *testing.T) { if r.sessionID != byokSessionID { t.Fatalf("BYOK inference request must carry session id %q, got %q", byokSessionID, r.sessionID) } + assertAgentMetadata(t, r) } if byokSessionID == capiSessionID { diff --git a/go/internal/e2e/event_fidelity_e2e_test.go b/go/internal/e2e/event_fidelity_e2e_test.go index c48a4908a1..e7cc4bfb37 100644 --- a/go/internal/e2e/event_fidelity_e2e_test.go +++ b/go/internal/e2e/event_fidelity_e2e_test.go @@ -168,6 +168,7 @@ func TestEventFidelityE2E(t *testing.T) { if answer == nil { t.Fatal("Expected SendAndWait to return an assistant message") + return } if ad, ok := answer.Data.(*copilot.AssistantMessageData); !ok || !strings.Contains(ad.Content, "18") { t.Errorf("Expected answer to contain '18', got %v", answer.Data) diff --git a/go/internal/e2e/inprocess_ffi_e2e_test.go b/go/internal/e2e/inprocess_ffi_e2e_test.go new file mode 100644 index 0000000000..6923384a28 --- /dev/null +++ b/go/internal/e2e/inprocess_ffi_e2e_test.go @@ -0,0 +1,62 @@ +package e2e + +import ( + "testing" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" +) + +// TestInProcessFfiE2E is a smoke test for the in-process (FFI) transport. It +// starts a client that loads the native runtime cdylib next to the resolved CLI +// entrypoint, lets the native host spawn the worker, performs a purely local +// "ping" round-trip through the runtime, and stops cleanly. No auth or replay +// proxy is involved, so it needs no snapshot. +// +// Mirrors python/e2e/test_inprocess_ffi_e2e.py and +// nodejs/test/e2e/inprocess_ffi.e2e.test.ts. +func TestInProcessFfiE2E(t *testing.T) { + // Loading the native runtime cdylib (libnode) into this test process installs + // foreign signal handlers. On macOS the Go runtime then aborts when it reaps + // its own os/exec children (see ffihost signal re-arming). The in-process + // matrix cell already loads libnode for the whole suite and re-arms those + // handlers; the default (child-process) cell must never load it, so restrict + // this dedicated FFI smoke test to the in-process cell. + if !testharness.IsInProcessTransport() { + t.Skip("in-process FFI smoke test runs only under the inprocess transport cell") + } + + cliPath := testharness.CLIPath() + if cliPath == "" { + t.Fatal("CLI not found. Run 'npm install' in the nodejs directory first.") + } + t.Setenv("COPILOT_CLI_PATH", cliPath) + + t.Run("should start and connect over in-process FFI", func(t *testing.T) { + client := copilot.NewClient(&copilot.ClientOptions{ + Connection: copilot.InProcessConnection{}, + }) + t.Cleanup(func() { client.ForceStop() }) + + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Failed to start client over in-process FFI: %v", err) + } + + pong, err := client.Ping(t.Context(), "ffi message") + if err != nil { + t.Fatalf("Failed to ping: %v", err) + } + + if pong.Message != "pong: ffi message" { + t.Errorf("Expected pong.message to be 'pong: ffi message', got %q", pong.Message) + } + + if pong.Timestamp.IsZero() { + t.Errorf("Expected non-zero pong.timestamp, got %s", pong.Timestamp) + } + + if err := client.Stop(); err != nil { + t.Errorf("Expected no errors on stop, got %v", err) + } + }) +} diff --git a/go/internal/e2e/mcp_and_agents_e2e_test.go b/go/internal/e2e/mcp_and_agents_e2e_test.go index e4bd34e268..71a152ecaf 100644 --- a/go/internal/e2e/mcp_and_agents_e2e_test.go +++ b/go/internal/e2e/mcp_and_agents_e2e_test.go @@ -115,10 +115,7 @@ func TestMCPServersE2E(t *testing.T) { t.Run("should pass literal env values to MCP server subprocess", func(t *testing.T) { ctx.ConfigureForTest(t) - mcpServerPath, err := filepath.Abs("../../../test/harness/test-mcp-server.mjs") - if err != nil { - t.Fatalf("Failed to resolve test-mcp-server path: %v", err) - } + mcpServerPath := testharness.RepoPath("test", "harness", "test-mcp-server.mjs") mcpServerDir := filepath.Dir(mcpServerPath) mcpServers := map[string]copilot.MCPServerConfig{ diff --git a/go/internal/e2e/mcp_oauth_e2e_test.go b/go/internal/e2e/mcp_oauth_e2e_test.go index 7959043063..1e08b839c8 100644 --- a/go/internal/e2e/mcp_oauth_e2e_test.go +++ b/go/internal/e2e/mcp_oauth_e2e_test.go @@ -6,7 +6,6 @@ import ( "net/http" "os" "os/exec" - "path/filepath" "slices" "strings" "sync" @@ -306,17 +305,14 @@ type oauthMCPRequest struct { func startOAuthMCPServer(t *testing.T) string { t.Helper() - serverPath, err := filepath.Abs("../../../test/harness/test-mcp-oauth-server.mjs") - if err != nil { - t.Fatalf("Failed to resolve OAuth MCP server path: %v", err) - } + serverPath := testharness.RepoPath("test", "harness", "test-mcp-oauth-server.mjs") cmd := exec.Command("node", serverPath) cmd.Env = append(os.Environ(), "EXPECTED_TOKEN="+expectedMCPOAuthToken) stdout, err := cmd.StdoutPipe() if err != nil { t.Fatalf("Failed to pipe OAuth MCP server stdout: %v", err) } - var stderr strings.Builder + var stderr syncBuffer cmd.Stderr = &stderr if err := cmd.Start(); err != nil { t.Fatalf("Failed to start OAuth MCP server: %v", err) @@ -362,6 +358,26 @@ func stringValue(value *string) string { return *value } +// syncBuffer is a minimal io.Writer whose contents can be read concurrently. +// os/exec writes to cmd.Stderr on a separate goroutine, so reading a plain +// strings.Builder while the process is running is a data race (caught by -race). +type syncBuffer struct { + mu sync.Mutex + buf strings.Builder +} + +func (b *syncBuffer) Write(p []byte) (int, error) { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.Write(p) +} + +func (b *syncBuffer) String() string { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.String() +} + func fetchOAuthMCPRequests(t *testing.T, baseURL string) []oauthMCPRequest { t.Helper() diff --git a/go/internal/e2e/mcp_server_helpers_test.go b/go/internal/e2e/mcp_server_helpers_test.go index e7269b1e26..68e72b18bc 100644 --- a/go/internal/e2e/mcp_server_helpers_test.go +++ b/go/internal/e2e/mcp_server_helpers_test.go @@ -8,16 +8,14 @@ import ( "time" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" "github.com/github/copilot-sdk/go/rpc" ) func testMCPServers(t *testing.T, serverNames ...string) map[string]copilot.MCPServerConfig { t.Helper() - mcpServerPath, err := filepath.Abs("../../../test/harness/test-mcp-server.mjs") - if err != nil { - t.Fatalf("Failed to resolve test-mcp-server path: %v", err) - } + mcpServerPath := testharness.RepoPath("test", "harness", "test-mcp-server.mjs") mcpServerDir := filepath.Dir(mcpServerPath) mcpServers := make(map[string]copilot.MCPServerConfig, len(serverNames)) diff --git a/go/internal/e2e/mode_handlers_e2e_test.go b/go/internal/e2e/mode_handlers_e2e_test.go index 15800bf858..e7471fbd06 100644 --- a/go/internal/e2e/mode_handlers_e2e_test.go +++ b/go/internal/e2e/mode_handlers_e2e_test.go @@ -101,7 +101,7 @@ func TestModeHandlersE2E(t *testing.T) { if request.Summary != planSummary { t.Fatalf("Expected summary %q, got %q", planSummary, request.Summary) } - if len(request.Actions) != 3 || request.Actions[0] != "interactive" || request.Actions[1] != "autopilot" || request.Actions[2] != "exit_only" { + if len(request.Actions) != 3 || request.Actions[0] != "autopilot" || request.Actions[1] != "interactive" || request.Actions[2] != "exit_only" { t.Fatalf("Unexpected actions: %#v", request.Actions) } if request.RecommendedAction != "interactive" { diff --git a/go/internal/e2e/pre_mcp_tool_call_hook_e2e_test.go b/go/internal/e2e/pre_mcp_tool_call_hook_e2e_test.go index 1847270922..111cfb86a4 100644 --- a/go/internal/e2e/pre_mcp_tool_call_hook_e2e_test.go +++ b/go/internal/e2e/pre_mcp_tool_call_hook_e2e_test.go @@ -15,7 +15,7 @@ func TestPreMCPToolCallHookE2E(t *testing.T) { client := ctx.NewClient() t.Cleanup(func() { client.ForceStop() }) - testHarnessDir, _ := filepath.Abs("../../../test/harness") + testHarnessDir := testharness.RepoPath("test", "harness") metaEchoServer := filepath.Join(testHarnessDir, "test-mcp-meta-echo-server.mjs") metaEchoConfig := func() map[string]copilot.MCPServerConfig { diff --git a/go/internal/e2e/rpc_server_e2e_test.go b/go/internal/e2e/rpc_server_e2e_test.go index 2510eb1d9c..6ea9ad6851 100644 --- a/go/internal/e2e/rpc_server_e2e_test.go +++ b/go/internal/e2e/rpc_server_e2e_test.go @@ -604,6 +604,7 @@ func TestRPCServerE2E(t *testing.T) { projectSkillPath := findSkillDiscoveryPath(skillPaths.Paths, ctx.WorkDir) if projectSkillPath == nil { t.Fatalf("Expected skill discovery paths to include %q", ctx.WorkDir) + return } if strings.TrimSpace(projectSkillPath.Path) == "" { t.Fatal("Expected non-empty skill discovery path") @@ -632,6 +633,7 @@ func TestRPCServerE2E(t *testing.T) { projectAgentPath := findAgentDiscoveryPath(agentPaths.Paths, ctx.WorkDir) if projectAgentPath == nil { t.Fatalf("Expected agent discovery paths to include %q", ctx.WorkDir) + return } if strings.TrimSpace(projectAgentPath.Path) == "" { t.Fatal("Expected non-empty agent discovery path") diff --git a/go/internal/e2e/rpc_server_misc_e2e_test.go b/go/internal/e2e/rpc_server_misc_e2e_test.go index 38b569798c..37ec57e1ba 100644 --- a/go/internal/e2e/rpc_server_misc_e2e_test.go +++ b/go/internal/e2e/rpc_server_misc_e2e_test.go @@ -159,6 +159,7 @@ func TestRpcServerMisc(t *testing.T) { } if users == nil { t.Fatal("Expected non-nil users result") + return } for _, user := range *users { userInfo, ok := user.AuthInfo.(*rpc.UserAuthInfo) diff --git a/go/internal/e2e/rpc_server_plugins_e2e_test.go b/go/internal/e2e/rpc_server_plugins_e2e_test.go index 9dbd17a672..a9d1d243cc 100644 --- a/go/internal/e2e/rpc_server_plugins_e2e_test.go +++ b/go/internal/e2e/rpc_server_plugins_e2e_test.go @@ -58,6 +58,7 @@ func TestRpcServerPlugins(t *testing.T) { listed := findPortedInstalledPlugin(afterInstall.Plugins, portedPluginName, portedMarketplaceName) if listed == nil { t.Fatalf("Expected installed plugin %q in marketplace %q", portedPluginName, portedMarketplaceName) + return } if !listed.Enabled { t.Fatal("Expected listed marketplace plugin to be enabled") @@ -229,6 +230,7 @@ func TestRpcServerPlugins(t *testing.T) { mine := findPortedMarketplace(list.Marketplaces, portedMarketplaceName) if mine == nil { t.Fatalf("Expected marketplace %q in list %+v", portedMarketplaceName, list.Marketplaces) + return } if mine.IsDefault != nil && *mine.IsDefault { t.Fatal("Expected local marketplace not to be marked default") @@ -311,7 +313,10 @@ func newStartedPortedClient(t *testing.T, ctx *testharness.TestContext, opts ... func newStartedIsolatedPortedClient(t *testing.T, ctx *testharness.TestContext) *copilot.Client { t.Helper() - home := t.TempDir() + home, err := os.MkdirTemp(ctx.WorkDir, "plugin-home-") + if err != nil { + t.Fatalf("Failed to create isolated plugin home: %v", err) + } return newStartedPortedClient(t, ctx, func(opts *copilot.ClientOptions) { opts.Env = append(opts.Env, "COPILOT_HOME="+home, diff --git a/go/internal/e2e/rpc_ui_ephemeral_query_e2e_test.go b/go/internal/e2e/rpc_ui_ephemeral_query_e2e_test.go index c6e4033609..2669faea9a 100644 --- a/go/internal/e2e/rpc_ui_ephemeral_query_e2e_test.go +++ b/go/internal/e2e/rpc_ui_ephemeral_query_e2e_test.go @@ -26,6 +26,7 @@ func TestRpcUiEphemeralQuery(t *testing.T) { } if result == nil { t.Fatal("Expected non-nil ephemeral query result") + return } if strings.TrimSpace(result.Answer) == "" { t.Fatal("Expected non-empty ephemeral query answer") diff --git a/go/internal/e2e/rpc_workspace_checkpoints_e2e_test.go b/go/internal/e2e/rpc_workspace_checkpoints_e2e_test.go index 87c3c8da83..849e3a5fa6 100644 --- a/go/internal/e2e/rpc_workspace_checkpoints_e2e_test.go +++ b/go/internal/e2e/rpc_workspace_checkpoints_e2e_test.go @@ -33,6 +33,11 @@ func TestRPCWorkspaceCheckpointsE2E(t *testing.T) { }) t.Run("should return nil or empty content for unknown checkpoint", func(t *testing.T) { + // In-process, session.workspaces.readCheckpoint is answered by the native + // runtime, which decodes the checkpoint number as a u32 and rejects the + // large sentinel this test uses. Covered by the default (stdio) transport. + // Mirrors Rust's should_return_null_or_empty_content_for_unknown_checkpoint. + testharness.SkipIfInProcess(t, "readCheckpoint decodes the id as u32 in-process") session := createWorkspaceRPCSession(t, client) defer session.Disconnect() diff --git a/go/internal/e2e/session_config_e2e_test.go b/go/internal/e2e/session_config_e2e_test.go index 6dc1c59ad4..672a905dc0 100644 --- a/go/internal/e2e/session_config_e2e_test.go +++ b/go/internal/e2e/session_config_e2e_test.go @@ -404,6 +404,7 @@ func TestSessionConfigNewOptionsE2E(t *testing.T) { } func TestSessionConfigNewOptionsCopilotRequestE2E(t *testing.T) { + testharness.SkipIfInProcess(t, "an LLM inference provider is process-global in-process") t.Run("should enable citations for Anthropic file attachments on create", func(t *testing.T) { ctx := testharness.NewTestContext(t) transport := &recordingTransport{} diff --git a/go/internal/e2e/subagent_hooks_e2e_test.go b/go/internal/e2e/subagent_hooks_e2e_test.go index c632b1e606..0e2fde9f86 100644 --- a/go/internal/e2e/subagent_hooks_e2e_test.go +++ b/go/internal/e2e/subagent_hooks_e2e_test.go @@ -1,6 +1,7 @@ package e2e import ( + "net/http" "os" "path/filepath" "sync" @@ -10,10 +11,78 @@ import ( "github.com/github/copilot-sdk/go/internal/e2e/testharness" ) +type subagentRequestRecord struct { + agentID string + parentAgentID string + interactionType string +} + +type recordingForwardingTransport struct { + inner http.RoundTripper + mu sync.Mutex + records []subagentRequestRecord +} + +func newRecordingForwardingTransport() *recordingForwardingTransport { + inner := http.DefaultTransport.(*http.Transport).Clone() + inner.DisableCompression = true + return &recordingForwardingTransport{inner: inner} +} + +func (rt *recordingForwardingTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if isInferenceURL(req.URL.String()) { + rctx := copilot.RequestContextFrom(req) + record := subagentRequestRecord{} + if rctx != nil { + record.agentID = rctx.AgentID + record.parentAgentID = rctx.ParentAgentID + record.interactionType = rctx.InteractionType + } + rt.mu.Lock() + rt.records = append(rt.records, record) + rt.mu.Unlock() + } + return rt.inner.RoundTrip(req) +} + +func (rt *recordingForwardingTransport) inferenceRecords() []subagentRequestRecord { + rt.mu.Lock() + defer rt.mu.Unlock() + out := make([]subagentRequestRecord, len(rt.records)) + copy(out, rt.records) + return out +} + +func assertSubagentRequestMetadata(t *testing.T, records []subagentRequestRecord) { + t.Helper() + if len(records) == 0 { + t.Fatal("request handler should observe inference requests") + } + for _, r := range records { + if r.parentAgentID == "" { + continue + } + if r.agentID == "" { + t.Fatal("sub-agent inference request should carry an agent id") + } + if r.interactionType == "" { + t.Fatal("sub-agent inference request should carry an interaction type") + } + if r.parentAgentID == r.agentID { + t.Fatal("sub-agent inference request should have distinct parent and child agent ids") + } + return + } + t.Fatal("sub-agent inference request should carry a parent agent id") +} + func TestSubagentHooksE2E(t *testing.T) { + testharness.SkipIfInProcess(t, "an LLM inference provider is process-global in-process") ctx := testharness.NewTestContext(t) + transport := newRecordingForwardingTransport() client := ctx.NewClient(func(o *copilot.ClientOptions) { o.Env = append(o.Env, "COPILOT_EXP_COPILOT_CLI_SESSION_BASED_SUBAGENTS=true") + o.RequestHandler = &copilot.CopilotRequestHandler{Transport: transport} }) t.Cleanup(func() { client.ForceStop() }) @@ -100,5 +169,6 @@ func TestSubagentHooksE2E(t *testing.T) { if viewPre[0].sessionID == taskPre.sessionID { t.Error("Sub-agent tool hooks should have a different sessionId than parent tool hooks") } + assertSubagentRequestMetadata(t, transport.inferenceRecords()) }) } diff --git a/go/internal/e2e/system_message_sections_e2e_test.go b/go/internal/e2e/system_message_sections_e2e_test.go index 61493c8122..c1eb313a25 100644 --- a/go/internal/e2e/system_message_sections_e2e_test.go +++ b/go/internal/e2e/system_message_sections_e2e_test.go @@ -43,6 +43,7 @@ func TestSystemMessageSectionsE2E(t *testing.T) { } if response == nil { t.Fatal("Expected a response from the assistant") + return } ad, ok := response.Data.(*copilot.AssistantMessageData) @@ -82,6 +83,7 @@ func TestSystemMessageSectionsE2E(t *testing.T) { } if response == nil { t.Fatal("Expected a response from the assistant") + return } ad, ok := response.Data.(*copilot.AssistantMessageData) diff --git a/go/internal/e2e/telemetry_e2e_test.go b/go/internal/e2e/telemetry_e2e_test.go index eff384d020..4567817fd9 100644 --- a/go/internal/e2e/telemetry_e2e_test.go +++ b/go/internal/e2e/telemetry_e2e_test.go @@ -14,6 +14,7 @@ import ( // Mirrors dotnet/test/TelemetryExportTests.cs (snapshot category "telemetry"). func TestTelemetryE2E(t *testing.T) { + testharness.SkipIfInProcess(t, "telemetry configuration is not honored in-process") t.Run("should export file telemetry for sdk interactions", func(t *testing.T) { ctx := testharness.NewTestContext(t) ctx.ConfigureForTest(t) diff --git a/go/internal/e2e/testharness/context.go b/go/internal/e2e/testharness/context.go index 6b6463749f..b73153dfff 100644 --- a/go/internal/e2e/testharness/context.go +++ b/go/internal/e2e/testharness/context.go @@ -33,13 +33,11 @@ func CLIPath() string { // 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, err := filepath.Abs("../../../nodejs/node_modules/@github") - if err == nil { - matches, _ := filepath.Glob(filepath.Join(base, "copilot-*", "index.js")) - if len(matches) > 0 { - cliPath = matches[0] - return - } + base := RepoPath("nodejs", "node_modules", "@github") + matches, _ := filepath.Glob(filepath.Join(base, "copilot-*", "index.js")) + if len(matches) > 0 { + cliPath = matches[0] + return } }) return cliPath @@ -53,6 +51,67 @@ type TestContext struct { ProxyURL string proxy *CapiProxy + + // In-process transport state. When the inprocess CI matrix cell is active the + // worker inherits this process's ambient env and cwd (per-client env/working + // directory are rejected in-process), so the isolated test env/cwd are mirrored + // onto the real process and restored on Close. + inProcess bool + restoreEnv []envRestore + restoreCwd string +} + +// envRestore captures a single environment variable's prior value so the +// in-process ambient mirror can be undone during teardown. +type envRestore struct { + key string + prev string + had bool +} + +// isInProcessTransport reports whether the in-process (FFI) transport is selected +// for E2E tests via COPILOT_SDK_DEFAULT_CONNECTION=inprocess. Mirrors the +// Node/Python/.NET harnesses. +func isInProcessTransport() bool { + return strings.EqualFold(os.Getenv("COPILOT_SDK_DEFAULT_CONNECTION"), "inprocess") +} + +// init neutralizes any ambient HMAC signing key as early as package load when the +// in-process transport is selected. Host-side auth resolution ranks the HMAC key +// above the GitHub token, so an ambient COPILOT_HMAC_KEY (CI injects one as a +// job-level credential) would be picked over the token the replay snapshots +// expect, producing request signatures that miss the recorded exchanges. Because +// the runtime is hosted in this process, the key must be removed before the native +// library is loaded and captures it — a later, per-client override is too late and +// setting it to an empty value is still treated as a signing key. Out-of-process +// children resolve auth in their own process where the token already outranks the +// HMAC key, so this is scoped to the in-process cell. Mirrors the analogous +// module-load neutralization in the Node/Python/.NET harnesses. +// See https://github.com/github/copilot-sdk/issues/1934. +func init() { + if isInProcessTransport() { + os.Unsetenv("COPILOT_HMAC_KEY") + os.Unsetenv("CAPI_HMAC_KEY") + } +} + +// IsInProcessTransport reports whether E2E tests run under the in-process (FFI) +// transport. Tests that configure options unsupported in-process (e.g. per-client +// telemetry) should skip when this returns true. +func IsInProcessTransport() bool { + return isInProcessTransport() +} + +// SkipIfInProcess skips the test when E2E tests run under the in-process (FFI) +// transport, for behavior the shared in-process runtime cannot support (e.g. a +// process-global LLM inference provider, or per-client telemetry). The reason is +// surfaced in the test log so the skip is explicit rather than a silent transport +// downgrade. Such tests still run over stdio in the default matrix cell. +func SkipIfInProcess(t *testing.T, reason string) { + t.Helper() + if isInProcessTransport() { + t.Skipf("unsupported over the in-process (FFI) transport: %s", reason) + } } // NewTestContext creates a new test context with isolated directories and a replaying proxy. @@ -106,11 +165,12 @@ func NewTestContext(t *testing.T) *TestContext { } ctx := &TestContext{ - CLIPath: cliPath, - HomeDir: homeDir, - WorkDir: workDir, - ProxyURL: proxyURL, - proxy: proxy, + CLIPath: cliPath, + HomeDir: homeDir, + WorkDir: workDir, + ProxyURL: proxyURL, + proxy: proxy, + inProcess: isInProcessTransport(), } t.Cleanup(func() { @@ -146,7 +206,14 @@ func (c *TestContext) ConfigureForTest(t *testing.T) { t.Fatalf("Expected test name with subtest, got: %s", testName) } sanitizedName := strings.ToLower(regexp.MustCompile(`[^a-zA-Z0-9]`).ReplaceAllString(parts[1], "_")) - snapshotPath := filepath.Join("..", "..", "..", "test", "snapshots", testFile, sanitizedName+".yaml") + // Anchor the snapshot path to the caller's source directory rather than the + // process working directory: the in-process transport chdir's into the test's + // isolated work dir (the worker inherits the process cwd), so a cwd-relative + // path would resolve against the wrong root for every subtest after the first. + // All e2e test files live in go/internal/e2e, so the repo root is three levels + // up from the caller's directory. + repoRoot := filepath.Join(filepath.Dir(callerFile), "..", "..", "..") + snapshotPath := filepath.Join(repoRoot, "test", "snapshots", testFile, sanitizedName+".yaml") absSnapshotPath, err := filepath.Abs(snapshotPath) if err != nil { @@ -172,6 +239,7 @@ func (c *TestContext) ConfigureWithoutSnapshot(t *testing.T) { // Close cleans up the test context resources. func (c *TestContext) Close(testFailed bool) { + c.restoreInProcessEnvironment() if c.proxy != nil { c.proxy.StopWithOptions(testFailed) } @@ -183,6 +251,62 @@ func (c *TestContext) Close(testFailed bool) { } } +// applyInProcessEnvironment mirrors the isolated test environment onto the real +// process for in-process hosting: the worker inherits this process's env and cwd +// at spawn, so per-test redirects must live on os.Environ and the process cwd. +// Auth flows via GH_TOKEN/GITHUB_TOKEN (the FFI argv omits the stdio auth-token +// wiring); the ambient HMAC signing key is removed process-wide at package load +// (see init) so host-side auth matches the replay snapshots. mergedEnv is the +// effective per-client env (harness defaults plus any per-test additions); workDir +// is the effective working directory. Values are restored in Close. Safe to call +// more than once (restores unwind in reverse). +func (c *TestContext) applyInProcessEnvironment(mergedEnv []string, workDir string) { + inprocessEnv := map[string]string{} + for _, kv := range mergedEnv { + if key, value, ok := strings.Cut(kv, "="); ok { + inprocessEnv[key] = value + } + } + // Auth flows via GH_TOKEN/GITHUB_TOKEN for the in-process host, overriding any + // inherited values. The HMAC key is neutralized process-wide at package load. + inprocessEnv["GH_TOKEN"] = defaultGitHubToken + inprocessEnv["GITHUB_TOKEN"] = defaultGitHubToken + inprocessEnv["COPILOT_CLI_PATH"] = c.CLIPath + delete(inprocessEnv, "COPILOT_HMAC_KEY") + delete(inprocessEnv, "CAPI_HMAC_KEY") + + for key, value := range inprocessEnv { + prev, had := os.LookupEnv(key) + c.restoreEnv = append(c.restoreEnv, envRestore{key: key, prev: prev, had: had}) + os.Setenv(key, value) + } + if workDir != "" { + if c.restoreCwd == "" { + if cwd, err := os.Getwd(); err == nil { + c.restoreCwd = cwd + } + } + os.Chdir(workDir) + } +} + +// restoreInProcessEnvironment undoes applyInProcessEnvironment during teardown. +func (c *TestContext) restoreInProcessEnvironment() { + for i := len(c.restoreEnv) - 1; i >= 0; i-- { + r := c.restoreEnv[i] + if r.had { + os.Setenv(r.key, r.prev) + } else { + os.Unsetenv(r.key) + } + } + c.restoreEnv = nil + if c.restoreCwd != "" { + os.Chdir(c.restoreCwd) + c.restoreCwd = "" + } +} + // GetExchanges retrieves the captured HTTP exchanges from the proxy. func (c *TestContext) GetExchanges() ([]ParsedHttpExchange, error) { return c.proxy.GetExchanges() @@ -261,9 +385,39 @@ func (c *TestContext) NewClient(opts ...func(*copilot.ClientOptions)) *copilot.C options.GitHubToken = defaultGitHubToken } + // Under the inprocess matrix cell, host the default stdio connection in-process. + // The worker inherits this process's ambient env/cwd (per-client env and working + // directory are rejected in-process), so mirror the effective (merged) env and + // cwd onto the real process and drop those options. Tests that pin a specific + // transport (TCP/URI/custom stdio) or configure per-client telemetry are left on + // their transport, mirroring the Node/.NET harnesses. + if c.inProcess && c.shouldUseInProcess(options) { + c.applyInProcessEnvironment(options.Env, options.WorkingDirectory) + options.Connection = copilot.InProcessConnection{} + options.Env = nil + options.WorkingDirectory = "" + } + return copilot.NewClient(options) } +// shouldUseInProcess reports whether a client built from options should be hosted +// in-process for the inprocess matrix cell. Only the harness default stdio +// connection is swapped; a test that pins a custom stdio path/args/env or a +// TCP/URI connection is exercising behavior that must stay on its own transport. +// +// Options the in-process runtime cannot support (per-client telemetry, an LLM +// inference provider) are NOT silently downgraded here — the affected tests skip +// explicitly via testharness.SkipIfInProcess so the limitation is visible rather +// than masked by a quiet transport swap. +func (c *TestContext) shouldUseInProcess(options *copilot.ClientOptions) bool { + s, ok := options.Connection.(copilot.StdioConnection) + if !ok { + return false + } + return s.Path == c.CLIPath && len(s.Args) == 0 && s.Env == nil +} + func fileExists(path string) bool { _, err := os.Stat(path) return err == nil diff --git a/go/internal/e2e/testharness/helper.go b/go/internal/e2e/testharness/helper.go index ca94d03adf..af08b2dbcc 100644 --- a/go/internal/e2e/testharness/helper.go +++ b/go/internal/e2e/testharness/helper.go @@ -3,11 +3,32 @@ package testharness import ( "context" "errors" + "path/filepath" + "runtime" "time" copilot "github.com/github/copilot-sdk/go" ) +// RepoPath resolves a path relative to the repository root, anchored to this +// source file's directory rather than the process working directory. The +// in-process (FFI) transport os.Chdir's the whole test process into a per-test +// temp workdir (the shared runtime host inherits the process cwd), so any +// cwd-relative resolution (e.g. filepath.Abs("../../../test/...")) would break +// for every test after the first in-process one. This helper stays correct +// regardless of the current working directory. +func RepoPath(elem ...string) string { + _, callerFile, _, ok := runtime.Caller(0) + if !ok { + // Fall back to a cwd-relative join; only correct before any chdir. + return filepath.Join(append([]string{"..", "..", ".."}, elem...)...) + } + // This file lives at go/internal/e2e/testharness/, so the repo root is four + // levels up from its directory. + repoRoot := filepath.Join(filepath.Dir(callerFile), "..", "..", "..", "..") + return filepath.Join(append([]string{repoRoot}, elem...)...) +} + // GetFinalAssistantMessage waits for and returns the final assistant message from a session turn. // If alreadyIdle is true, skip waiting for session.idle (useful for resumed sessions where the // idle event was ephemeral and not persisted in the event history). diff --git a/go/internal/e2e/testharness/proxy.go b/go/internal/e2e/testharness/proxy.go index 8933183c87..ec16124bc6 100644 --- a/go/internal/e2e/testharness/proxy.go +++ b/go/internal/e2e/testharness/proxy.go @@ -38,11 +38,14 @@ func (p *CapiProxy) Start() (string, error) { return p.proxyURL, nil } - // The harness server is in the shared test directory - serverPath := "../../../test/harness/server.ts" + // The harness server is in the shared test directory. Anchor the path to + // the repo root (not the process cwd), because the in-process (FFI) + // transport os.Chdir's into a per-test temp workdir, which would otherwise + // break the cwd-relative resolution. + serverPath := RepoPath("test", "harness", "server.ts") p.cmd = exec.Command("npx", "tsx", serverPath) - p.cmd.Dir = "." // Will be resolved relative to test execution + p.cmd.Dir = RepoPath("test", "harness") stdout, err := p.cmd.StdoutPipe() if err != nil { diff --git a/go/internal/e2e/tools_e2e_test.go b/go/internal/e2e/tools_e2e_test.go index 1600eb1630..062d377917 100644 --- a/go/internal/e2e/tools_e2e_test.go +++ b/go/internal/e2e/tools_e2e_test.go @@ -140,6 +140,7 @@ func TestToolsE2E(t *testing.T) { if answer == nil { t.Fatalf("Expected non-nil assistant message") + return } ad, ok := answer.Data.(*copilot.AssistantMessageData) if !ok { diff --git a/go/internal/embeddedcli/embeddedcli.go b/go/internal/embeddedcli/embeddedcli.go index 0866a3f811..cd0be21895 100644 --- a/go/internal/embeddedcli/embeddedcli.go +++ b/go/internal/embeddedcli/embeddedcli.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -18,15 +19,30 @@ import ( // Config defines the inputs used to install and locate the embedded Copilot CLI. // // Cli and CliHash are required. If Dir is empty, the CLI is installed into the -// system cache directory. Version is used to suffix the installed binary name to -// allow multiple versions to coexist. License, when provided, is written next -// to the installed binary. +// system cache directory. When Version is set, the CLI is installed into a +// version-specific child directory so multiple versions can coexist. License, +// when provided, is written next to the installed binary. +// +// RuntimeLib and RuntimeLibHash are optional: when set, the native in-process +// runtime library (cdylib) is installed next to the CLI binary so the in-process +// (FFI) transport can load it. They are omitted for CLI packages that do not +// ship the native runtime. type Config struct { Cli io.Reader CliHash []byte License []byte + RuntimeLib io.Reader + RuntimeLibHash []byte + + // LinuxMuslCli and LinuxMuslRuntimeLib are optional alternatives selected + // automatically when the application runs on a musl-based Linux system. + LinuxMuslCli io.Reader + LinuxMuslCliHash []byte + LinuxMuslRuntimeLib io.Reader + LinuxMuslRuntimeLibHash []byte + Dir string Version string } @@ -38,6 +54,12 @@ func Setup(cfg Config) { if len(cfg.CliHash) != sha256.Size { panic(fmt.Sprintf("CliHash must be a SHA-256 hash (%d bytes), got %d bytes", sha256.Size, len(cfg.CliHash))) } + if cfg.LinuxMuslCli != nil && len(cfg.LinuxMuslCliHash) != sha256.Size { + panic(fmt.Sprintf("LinuxMuslCliHash must be a SHA-256 hash (%d bytes), got %d bytes", sha256.Size, len(cfg.LinuxMuslCliHash))) + } + if cfg.LinuxMuslRuntimeLib != nil && len(cfg.LinuxMuslRuntimeLibHash) != sha256.Size { + panic(fmt.Sprintf("LinuxMuslRuntimeLibHash must be a SHA-256 hash (%d bytes), got %d bytes", sha256.Size, len(cfg.LinuxMuslRuntimeLibHash))) + } setupMu.Lock() defer setupMu.Unlock() if setupDone { @@ -61,14 +83,28 @@ var Path = sync.OnceValue(func() string { return path }) +// RuntimeLibPath returns the on-disk path to the installed native in-process +// runtime library (cdylib), or "" when no runtime library was bundled or the +// CLI could not be installed. It ensures the embedded CLI is installed first. +func RuntimeLibPath() string { + Path() + setupMu.Lock() + defer setupMu.Unlock() + return runtimeLibPath +} + var ( config Config setupMu sync.Mutex setupDone bool pathInitialized bool + runtimeLibPath string + linuxMuslBundle bool ) func install() (path string) { + selectLinuxMuslBundle() + verbose := os.Getenv("COPILOT_CLI_INSTALL_VERBOSE") == "1" logError := func(msg string, err error) { if verbose { @@ -103,18 +139,41 @@ func install() (path string) { return path } -func installAt(installDir string) (string, error) { - if err := os.MkdirAll(installDir, 0755); err != nil { - return "", fmt.Errorf("creating install directory: %w", err) +func selectLinuxMuslBundle() { + if runtime.GOOS != "linux" || config.LinuxMuslCli == nil || !isMusl() { + return } + config = linuxMuslConfig(config) + linuxMuslBundle = true +} + +func linuxMuslConfig(cfg Config) Config { + cfg.Cli = cfg.LinuxMuslCli + cfg.CliHash = cfg.LinuxMuslCliHash + cfg.RuntimeLib = cfg.LinuxMuslRuntimeLib + cfg.RuntimeLibHash = cfg.LinuxMuslRuntimeLibHash + return cfg +} + +func isMusl() bool { + out, _ := exec.Command("ldd", "--version").CombinedOutput() + return strings.Contains(strings.ToLower(string(out)), "musl") +} + +func installAt(installDir string) (string, error) { version := sanitizeVersion(config.Version) - lockName := ".copilot-cli.lock" if version != "" { - lockName = fmt.Sprintf(".copilot-cli-%s.lock", version) + installDir = filepath.Join(installDir, version) + } + if linuxMuslBundle { + installDir = filepath.Join(installDir, "linuxmusl") + } + if err := os.MkdirAll(installDir, 0755); err != nil { + return "", fmt.Errorf("creating install directory: %w", err) } // Best effort to prevent concurrent installs. - if release, _ := flock.Acquire(filepath.Join(installDir, lockName)); release != nil { + if release, _ := flock.Acquire(filepath.Join(installDir, ".copilot-cli.lock")); release != nil { defer release() } @@ -122,7 +181,7 @@ func installAt(installDir string) (string, error) { if runtime.GOOS == "windows" { binaryName += ".exe" } - finalPath := versionedBinaryPath(installDir, binaryName, version) + finalPath := filepath.Join(installDir, binaryName) if _, err := os.Stat(finalPath); err == nil { existingHash, err := hashFile(finalPath) @@ -132,6 +191,13 @@ func installAt(installDir string) (string, error) { if !bytes.Equal(existingHash, config.CliHash) { return "", fmt.Errorf("existing binary hash mismatch") } + if config.RuntimeLib != nil { + libPath, err := installRuntimeLib(installDir) + if err != nil { + return "", err + } + runtimeLibPath = libPath + } return finalPath, nil } @@ -155,17 +221,81 @@ func installAt(installDir string) (string, error) { return "", fmt.Errorf("writing license file: %w", err) } } + + // 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 { + libPath, err := installRuntimeLib(installDir) + if err != nil { + return "", err + } + runtimeLibPath = libPath + } + return finalPath, nil } -// versionedBinaryPath builds the unpacked binary filename with an optional version suffix. -func versionedBinaryPath(dir, binaryName, version string) string { - if version == "" { - return filepath.Join(dir, binaryName) +// installRuntimeLib writes the embedded runtime cdylib into installDir under its +// natural platform file name, verifying its SHA-256. It is idempotent: an +// existing file with a matching hash is reused; a mismatch is a hard error. +func installRuntimeLib(installDir string) (string, error) { + if len(config.RuntimeLibHash) != sha256.Size { + return "", fmt.Errorf("RuntimeLibHash must be a SHA-256 hash (%d bytes), got %d bytes", sha256.Size, len(config.RuntimeLibHash)) + } + libPath := filepath.Join(installDir, naturalRuntimeLibName()) + + if _, err := os.Stat(libPath); err == nil { + existingHash, err := hashFile(libPath) + if err != nil { + return "", fmt.Errorf("hashing existing runtime library: %w", err) + } + if !bytes.Equal(existingHash, config.RuntimeLibHash) { + return "", fmt.Errorf("existing runtime library hash mismatch") + } + return libPath, nil + } + + // Write to a temp file in the same directory, verify, then atomically rename. + tmp, err := os.CreateTemp(installDir, ".copilot-runtime-*.tmp") + if err != nil { + return "", fmt.Errorf("creating temp runtime library: %w", err) + } + tmpPath := tmp.Name() + h := sha256.New() + _, err = io.Copy(io.MultiWriter(tmp, h), config.RuntimeLib) + if err1 := tmp.Close(); err1 != nil && err == nil { + err = err1 + } + if closer, ok := config.RuntimeLib.(io.Closer); ok { + closer.Close() + } + if err != nil { + os.Remove(tmpPath) + return "", fmt.Errorf("writing runtime library: %w", err) + } + if !bytes.Equal(h.Sum(nil), config.RuntimeLibHash) { + os.Remove(tmpPath) + return "", fmt.Errorf("runtime library hash mismatch") + } + if err := os.Rename(tmpPath, libPath); err != nil { + os.Remove(tmpPath) + return "", fmt.Errorf("installing runtime library: %w", err) + } + return libPath, nil +} + +// naturalRuntimeLibName is the flat platform file name for the runtime cdylib, +// matching ffihost.NaturalLibraryName (kept in sync; embeddedcli stays +// dependency-free for use by generated embed files). +func naturalRuntimeLibName() string { + switch runtime.GOOS { + case "windows": + return "copilot_runtime.dll" + case "darwin": + return "libcopilot_runtime.dylib" + default: + return "libcopilot_runtime.so" } - base := strings.TrimSuffix(binaryName, filepath.Ext(binaryName)) - ext := filepath.Ext(binaryName) - return filepath.Join(dir, fmt.Sprintf("%s_%s%s", base, version, ext)) } // sanitizeVersion makes a version string safe for filenames. @@ -188,7 +318,11 @@ func sanitizeVersion(version string) string { b.WriteRune('_') } } - return b.String() + sanitized := b.String() + if sanitized == "." || sanitized == ".." { + return strings.Repeat("_", len(sanitized)) + } + return sanitized } // hashFile returns the SHA-256 hash of a file on disk. diff --git a/go/internal/embeddedcli/embeddedcli_test.go b/go/internal/embeddedcli/embeddedcli_test.go index 0453f7293d..b0394e0f69 100644 --- a/go/internal/embeddedcli/embeddedcli_test.go +++ b/go/internal/embeddedcli/embeddedcli_test.go @@ -16,6 +16,8 @@ func resetGlobals() { config = Config{} setupDone = false pathInitialized = false + runtimeLibPath = "" + linuxMuslBundle = false } func mustPanic(t *testing.T, fn func()) { @@ -36,6 +38,31 @@ func binaryNameForOS() string { return name } +func TestLinuxMuslConfigSelectsAlternativeArtifacts(t *testing.T) { + glibcCLI := strings.NewReader("glibc-cli") + glibcRuntime := strings.NewReader("glibc-runtime") + muslCLI := strings.NewReader("musl-cli") + muslRuntime := strings.NewReader("musl-runtime") + muslCLIHash := bytes.Repeat([]byte{1}, sha256.Size) + muslRuntimeHash := bytes.Repeat([]byte{2}, sha256.Size) + + selected := linuxMuslConfig(Config{ + Cli: glibcCLI, + RuntimeLib: glibcRuntime, + LinuxMuslCli: muslCLI, + LinuxMuslCliHash: muslCLIHash, + LinuxMuslRuntimeLib: muslRuntime, + LinuxMuslRuntimeLibHash: muslRuntimeHash, + }) + + if selected.Cli != muslCLI || selected.RuntimeLib != muslRuntime { + t.Fatal("Expected Linux musl artifacts to replace the glibc artifacts") + } + if !bytes.Equal(selected.CliHash, muslCLIHash) || !bytes.Equal(selected.RuntimeLibHash, muslRuntimeHash) { + t.Fatal("Expected Linux musl hashes to replace the glibc hashes") + } +} + func TestSetupPanicsOnNilCli(t *testing.T) { resetGlobals() mustPanic(t, func() { Setup(Config{}) }) @@ -65,7 +92,7 @@ func TestInstallAtWritesBinaryAndLicense(t *testing.T) { path := Path() - expectedPath := versionedBinaryPath(tempDir, binaryNameForOS(), "1.2.3") + expectedPath := filepath.Join(tempDir, "1.2.3", binaryNameForOS()) if path != expectedPath { t.Fatalf("unexpected path: got %q want %q", path, expectedPath) } @@ -99,7 +126,7 @@ func TestInstallAtWritesBinaryAndLicense(t *testing.T) { func TestInstallAtExistingBinaryHashMismatch(t *testing.T) { resetGlobals() tempDir := t.TempDir() - binaryPath := versionedBinaryPath(tempDir, binaryNameForOS(), "") + binaryPath := filepath.Join(tempDir, binaryNameForOS()) if err := os.MkdirAll(filepath.Dir(binaryPath), 0755); err != nil { t.Fatalf("mkdir: %v", err) } @@ -120,17 +147,102 @@ func TestInstallAtExistingBinaryHashMismatch(t *testing.T) { } func TestSanitizeVersion(t *testing.T) { - got := sanitizeVersion("v1.2.3+build/abc") - want := "v1.2.3_build_abc" - if got != want { - t.Fatalf("sanitizeVersion() = %q want %q", got, want) + tests := map[string]string{ + "v1.2.3+build/abc": "v1.2.3_build_abc", + ".": "_", + "..": "__", + } + for input, want := range tests { + if got := sanitizeVersion(input); got != want { + t.Errorf("sanitizeVersion(%q) = %q want %q", input, got, want) + } } } -func TestVersionedBinaryPath(t *testing.T) { - got := versionedBinaryPath("/tmp", "copilot.exe", "1.0.0") - want := filepath.Join("/tmp", "copilot_1.0.0.exe") - if got != want { - t.Fatalf("versionedBinaryPath() = %q want %q", got, want) +func TestInstallAtAllowsMultipleRuntimeVersions(t *testing.T) { + resetGlobals() + tempDir := t.TempDir() + + installVersion := func(version string, cliContent, runtimeContent []byte) (string, string) { + t.Helper() + cliHash := sha256.Sum256(cliContent) + runtimeHash := sha256.Sum256(runtimeContent) + config = Config{ + Cli: bytes.NewReader(cliContent), + CliHash: cliHash[:], + RuntimeLib: bytes.NewReader(runtimeContent), + RuntimeLibHash: runtimeHash[:], + Version: version, + } + + cliPath, err := installAt(tempDir) + if err != nil { + t.Fatalf("install version %s: %v", version, err) + } + return cliPath, runtimeLibPath + } + + cli1, runtime1 := installVersion("1.0.0", []byte("cli-one"), []byte("runtime-one")) + cli2, runtime2 := installVersion("2.0.0", []byte("cli-two"), []byte("runtime-two")) + + if cli1 == cli2 { + t.Fatalf("Expected versioned CLI paths to differ, got %q", cli1) + } + if runtime1 == runtime2 { + t.Fatalf("Expected versioned runtime paths to differ, got %q", runtime1) + } + if got, want := filepath.Base(cli1), binaryNameForOS(); got != want { + t.Fatalf("First CLI filename = %q, want %q", got, want) + } + if got, want := filepath.Base(runtime1), naturalRuntimeLibName(); got != want { + t.Fatalf("First runtime filename = %q, want %q", got, want) + } + if got, want := filepath.Base(filepath.Dir(cli1)), "1.0.0"; got != want { + t.Fatalf("First CLI version directory = %q, want %q", got, want) + } + if filepath.Dir(cli1) != filepath.Dir(runtime1) { + t.Fatalf("CLI and runtime were installed in different directories: %q and %q", cli1, runtime1) + } + if got, err := os.ReadFile(runtime1); err != nil || string(got) != "runtime-one" { + t.Fatalf("Unexpected first runtime: content=%q err=%v", got, err) + } + if got, err := os.ReadFile(runtime2); err != nil || string(got) != "runtime-two" { + t.Fatalf("Unexpected second runtime: content=%q err=%v", got, err) + } +} + +func TestInstallAtExistingBinaryInstallsMissingRuntime(t *testing.T) { + resetGlobals() + tempDir := t.TempDir() + versionDir := filepath.Join(tempDir, "1.2.3") + if err := os.MkdirAll(versionDir, 0755); err != nil { + t.Fatalf("mkdir: %v", err) + } + + cliContent := []byte("cli") + cliPath := filepath.Join(versionDir, binaryNameForOS()) + if err := os.WriteFile(cliPath, cliContent, 0755); err != nil { + t.Fatalf("write CLI: %v", err) + } + cliHash := sha256.Sum256(cliContent) + runtimeContent := []byte("runtime") + runtimeHash := sha256.Sum256(runtimeContent) + config = Config{ + Cli: bytes.NewReader(cliContent), + CliHash: cliHash[:], + RuntimeLib: bytes.NewReader(runtimeContent), + RuntimeLibHash: runtimeHash[:], + Version: "1.2.3", + } + + gotCLIPath, err := installAt(tempDir) + if err != nil { + t.Fatalf("installAt(): %v", err) + } + if gotCLIPath != cliPath { + t.Fatalf("installAt() = %q, want %q", gotCLIPath, cliPath) + } + if got, err := os.ReadFile(filepath.Join(versionDir, naturalRuntimeLibName())); err != nil || string(got) != "runtime" { + t.Fatalf("Unexpected runtime: content=%q err=%v", got, err) } } diff --git a/go/internal/ffihost/buffer.go b/go/internal/ffihost/buffer.go new file mode 100644 index 0000000000..2185ce833d --- /dev/null +++ b/go/internal/ffihost/buffer.go @@ -0,0 +1,67 @@ +//go:build copilot_inprocess && (darwin || linux || windows) + +package ffihost + +import ( + "io" + "sync" +) + +// receiveBuffer is a thread-safe byte buffer that feeds blocking Read from a +// producer thread. The native outbound callback (invoked on a foreign runtime +// thread) appends frames via feed without ever blocking; the JSON-RPC reader +// goroutine drains them via Read, which blocks until data or EOF. +// +// It implements io.ReadCloser so it can be handed to jsonrpc2.NewClient as the +// server → client stream. +type receiveBuffer struct { + mu sync.Mutex + cond *sync.Cond + buf []byte + closed bool +} + +func newReceiveBuffer() *receiveBuffer { + rb := &receiveBuffer{} + rb.cond = sync.NewCond(&rb.mu) + return rb +} + +func (rb *receiveBuffer) feed(data []byte) { + rb.mu.Lock() + defer rb.mu.Unlock() + if rb.closed { + return + } + rb.buf = append(rb.buf, data...) + rb.cond.Broadcast() +} + +// Read blocks until at least one byte is available or the buffer is closed. +// It returns io.EOF only once the buffer is closed and fully drained. +func (rb *receiveBuffer) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + rb.mu.Lock() + defer rb.mu.Unlock() + for len(rb.buf) == 0 && !rb.closed { + rb.cond.Wait() + } + if len(rb.buf) == 0 { + return 0, io.EOF + } + n := copy(p, rb.buf) + rb.buf = rb.buf[n:] + return n, nil +} + +// Close marks the buffer closed; subsequent Reads drain remaining bytes then +// return io.EOF. Idempotent. +func (rb *receiveBuffer) Close() error { + rb.mu.Lock() + defer rb.mu.Unlock() + rb.closed = true + rb.cond.Broadcast() + return nil +} diff --git a/go/internal/ffihost/ffihost.go b/go/internal/ffihost/ffihost.go new file mode 100644 index 0000000000..30cd831281 --- /dev/null +++ b/go/internal/ffihost/ffihost.go @@ -0,0 +1,384 @@ +//go:build copilot_inprocess && (darwin || linux || windows) + +// Package ffihost hosts the Copilot runtime in-process by loading its native +// library and driving JSON-RPC over the runtime's C ABI. +// +// It pumps opaque LSP Content-Length-framed JSON-RPC bytes across the boundary: +// +// - client → server frames go to copilot_runtime_connection_write +// - server → client frames arrive on a native callback that feeds a +// thread-safe receive buffer read by the JSON-RPC client +// +// The existing internal/jsonrpc2 client handles framing unchanged — this is a +// transport swap, not a new protocol. Host exposes an io.WriteCloser (client → +// server) and io.ReadCloser (server → client) that plug straight into +// jsonrpc2.NewClient. +// +// The C ABI (shared with the .NET, Node.js, Python, and Rust SDKs): +// +// uint32 copilot_runtime_host_start(uint8 *argv, size_t argv_len, +// uint8 *env, size_t env_len); +// bool copilot_runtime_host_shutdown(uint32 server_id); +// uint32 copilot_runtime_connection_open(uint32 server_id, outbound cb, +// void *user_data, +// uint8 *a, size_t a_len, +// uint8 *b, size_t b_len, +// uint8 *c, size_t c_len); +// bool copilot_runtime_connection_write(uint32 conn_id, uint8 *bytes, size_t len); +// bool copilot_runtime_connection_close(uint32 conn_id); +// // outbound callback: +// void outbound(void *user_data, uint8 *bytes, size_t len); +// +// The native binding uses github.com/ebitengine/purego so the library is loaded +// at runtime with CGO disabled, preserving the SDK's pure-Go build and +// cross-compilation. +package ffihost + +import ( + "encoding/json" + "fmt" + "io" + "runtime" + "strings" + "sync" + "sync/atomic" + "unsafe" + + "github.com/ebitengine/purego" +) + +const symbolPrefix = "copilot_runtime_" + +// ffiLibrary binds the copilot_runtime_* C ABI exports of a loaded cdylib. +type ffiLibrary struct { + handle uintptr + hostStart func(argv unsafe.Pointer, argvLen uintptr, env unsafe.Pointer, envLen uintptr) uint32 + hostShutdown func(serverID uint32) bool + connectionOpen func(serverID uint32, cb uintptr, userData uintptr, a unsafe.Pointer, aLen uintptr, b unsafe.Pointer, bLen uintptr, c unsafe.Pointer, cLen uintptr) uint32 + connectionWrite func(connID uint32, bytes unsafe.Pointer, length uintptr) bool + connectionClose func(connID uint32) bool +} + +// The cdylib may only be loaded once per process; a second load of a different +// path is unsupported (matches the .NET/Node/Python/Rust hosts). Guard it here. +var ( + loadMu sync.Mutex + loadedLibrary *ffiLibrary + loadedLibraryPath string +) + +var ( + outboundCallbackOnce sync.Once + outboundCallbackHandle uintptr + outboundTargets sync.Map + nextOutboundToken atomic.Uint64 +) + +func sharedOutboundCallback() uintptr { + outboundCallbackOnce.Do(func() { + outboundCallbackHandle = purego.NewCallback(routeOutbound) + }) + return outboundCallbackHandle +} + +func routeOutbound(userData uintptr, bytesPtr uintptr, bytesLen uintptr) uintptr { + target, ok := outboundTargets.Load(userData) + if !ok { + return 0 + } + return target.(*Host).onOutbound(bytesPtr, bytesLen) +} + +func loadLibrary(libraryPath string) (lib *ffiLibrary, err error) { + loadMu.Lock() + defer loadMu.Unlock() + + if loadedLibrary != nil { + if loadedLibraryPath != libraryPath { + return nil, fmt.Errorf( + "an in-process FFI runtime library is already loaded from %q; loading a different library from %q in the same process is not supported", + loadedLibraryPath, libraryPath) + } + return loadedLibrary, nil + } + + handle, err := openLibrary(libraryPath) + if err != nil { + return nil, fmt.Errorf("loading FFI runtime library %q: %w", libraryPath, err) + } + + // RegisterLibFunc panics if a symbol is missing; convert that to an error so + // callers get a clean failure instead of a crash. + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("binding FFI runtime library %q: %v", libraryPath, r) + lib = nil + } + }() + + bound := &ffiLibrary{handle: handle} + purego.RegisterLibFunc(&bound.hostStart, handle, symbolPrefix+"host_start") + purego.RegisterLibFunc(&bound.hostShutdown, handle, symbolPrefix+"host_shutdown") + purego.RegisterLibFunc(&bound.connectionOpen, handle, symbolPrefix+"connection_open") + purego.RegisterLibFunc(&bound.connectionWrite, handle, symbolPrefix+"connection_write") + purego.RegisterLibFunc(&bound.connectionClose, handle, symbolPrefix+"connection_close") + + loadedLibrary = bound + loadedLibraryPath = libraryPath + return bound, nil +} + +// Host hosts the Copilot runtime in-process via its native C ABI. +// +// Construct with Create, call Start to open the FFI connection, wire +// Writer/Reader into jsonrpc2.NewClient, and call Dispose to tear everything +// down. +type Host struct { + libraryPath string + cliEntrypoint string + environment map[string]string + args []string + lib *ffiLibrary + + // lifecycleMu serializes native start/write/shutdown operations. hostStart + // cannot be interrupted, so Dispose waits for it before closing native IDs. + lifecycleMu sync.Mutex + // mu serializes disposal with native callbacks so the receive buffer cannot + // be fed after it is closed. + mu sync.Mutex + serverID uint32 + connectionID uint32 + disposed bool + // activeCallbacks counts outbound native callbacks currently executing. + activeCallbacks int + + recv *receiveBuffer + + callbackToken uintptr +} + +// Create resolves the native library and prepares the host. environment and +// args contain SDK-managed runtime options. +func Create(cliEntrypoint string, environment map[string]string, args []string) (*Host, error) { + libraryPath, err := ResolveLibraryPath(cliEntrypoint) + if err != nil { + return nil, err + } + lib, err := loadLibrary(libraryPath) + if err != nil { + return nil, err + } + return &Host{ + libraryPath: libraryPath, + cliEntrypoint: cliEntrypoint, + environment: environment, + args: append([]string(nil), args...), + lib: lib, + recv: newReceiveBuffer(), + }, nil +} + +// Start opens the FFI connection. Native startup may block, so callers should +// run it off any latency-sensitive goroutine. +func (h *Host) Start() error { + h.lifecycleMu.Lock() + defer h.lifecycleMu.Unlock() + + h.mu.Lock() + if h.disposed { + h.mu.Unlock() + return fmt.Errorf("the in-process runtime host is disposed") + } + h.mu.Unlock() + + argv := h.buildArgv() + env := h.buildEnv() + + var argvPtr, envPtr unsafe.Pointer + if len(argv) > 0 { + argvPtr = unsafe.Pointer(&argv[0]) + } + if len(env) > 0 { + envPtr = unsafe.Pointer(&env[0]) + } + + h.serverID = h.lib.hostStart(argvPtr, uintptr(len(argv)), envPtr, uintptr(len(env))) + // Keep the JSON buffers alive across the (synchronous) native call. + runtime.KeepAlive(argv) + runtime.KeepAlive(env) + if h.serverID == 0 { + return fmt.Errorf("copilot_runtime_host_start failed (library %q, entrypoint %q)", h.libraryPath, h.cliEntrypoint) + } + + // host_start spawned the worker child via libuv's uv_spawn, which installs a + // SIGCHLD handler without SA_ONSTACK on its first call. The Go runtime aborts + // ("non-Go code set up signal handler without SA_ONSTACK flag") when it later + // reaps one of its own os/exec children (e.g. a test-spawned MCP server) and + // the delivered SIGCHLD lands on a non-signal stack. Re-add SA_ONSTACK to that + // foreign handler now that it exists (implemented on darwin+linux; a no-op on + // other platforms, and before the first spawn there is nothing to fix — hence + // here rather than at library load). + rearmForeignSignalHandlers(h.lib.handle) + + callbackHandle := sharedOutboundCallback() + callbackToken := uintptr(nextOutboundToken.Add(1)) + outboundTargets.Store(callbackToken, h) + h.callbackToken = callbackToken + h.connectionID = h.lib.connectionOpen(h.serverID, callbackHandle, callbackToken, nil, 0, nil, 0, nil, 0) + if h.connectionID == 0 { + outboundTargets.Delete(callbackToken) + h.callbackToken = 0 + h.lib.hostShutdown(h.serverID) + rearmForeignSignalHandlers(h.lib.handle) + h.serverID = 0 + return fmt.Errorf("copilot_runtime_connection_open failed") + } + return nil +} + +// Writer returns the client → server frame sink (plug into jsonrpc2 as stdin). +func (h *Host) Writer() io.WriteCloser { return hostWriter{h} } + +// Reader returns the server → client frame source (plug into jsonrpc2 as stdout). +func (h *Host) Reader() io.ReadCloser { return h.recv } + +func (h *Host) buildArgv() []byte { + // A `.js` entrypoint (dev) is launched via node; the packaged single-file CLI + // embeds its own Node and is invoked directly. `--no-auto-update` pins the + // worker to the runtime package matching the loaded cdylib (avoids ABI skew). + var argv []string + if strings.HasSuffix(strings.ToLower(h.cliEntrypoint), ".js") { + argv = []string{"node", h.cliEntrypoint, "--embedded-host", "--no-auto-update"} + } else { + argv = []string{h.cliEntrypoint, "--embedded-host", "--no-auto-update"} + } + argv = append(argv, h.args...) + b, _ := json.Marshal(argv) + return b +} + +func (h *Host) buildEnv() []byte { + if len(h.environment) == 0 { + return nil + } + b, _ := json.Marshal(h.environment) + return b +} + +// onOutbound is the native server → client callback, invoked on a foreign +// runtime thread. The native pointer is only valid for this call, so the bytes +// are copied out before returning. Nothing may panic across the FFI boundary. +func (h *Host) onOutbound(bytesPtr uintptr, bytesLen uintptr) uintptr { + h.mu.Lock() + if h.disposed { + h.mu.Unlock() + return 0 + } + h.activeCallbacks++ + h.mu.Unlock() + + defer func() { + h.mu.Lock() + h.activeCallbacks-- + h.mu.Unlock() + // Never let a panic unwind into native code. + _ = recover() + }() + + if bytesPtr != 0 && bytesLen > 0 { + // The native runtime delivers the outbound frame as a raw buffer address + // (uintptr) plus length. Materialize a slice over it just long enough to + // copy the bytes into Go-owned memory before returning to native code. + //nolint:govet // FFI callback receives the buffer address as an integer; converting it to a pointer to copy out is the intended, checked-length use. + src := unsafe.Slice((*byte)(unsafe.Pointer(bytesPtr)), int(bytesLen)) + buf := make([]byte, len(src)) + copy(buf, src) + h.recv.feed(buf) + } + return 0 +} + +func (h *Host) writeFrame(frame []byte) (int, error) { + h.lifecycleMu.Lock() + defer h.lifecycleMu.Unlock() + + h.mu.Lock() + disposed := h.disposed + h.mu.Unlock() + connID := h.connectionID + if disposed || connID == 0 { + return 0, fmt.Errorf("the in-process runtime connection is closed") + } + if len(frame) == 0 { + return 0, nil + } + ok := h.lib.connectionWrite(connID, unsafe.Pointer(&frame[0]), uintptr(len(frame))) + runtime.KeepAlive(frame) + if !ok { + return 0, fmt.Errorf("failed to write a frame to the in-process runtime connection") + } + return len(frame), nil +} + +// Dispose closes the FFI connection, shuts down the native host, and releases +// resources. It is idempotent and waits for any in-flight outbound callback to +// finish before closing the receive buffer. +func (h *Host) Dispose() { + h.lifecycleMu.Lock() + defer h.lifecycleMu.Unlock() + + h.mu.Lock() + if h.disposed { + h.mu.Unlock() + return + } + // Publish disposed under the same lock onOutbound uses to check it, so no new + // callback can pass the check and increment activeCallbacks after the drain + // loop below observes zero. + h.disposed = true + connID := h.connectionID + serverID := h.serverID + callbackToken := h.callbackToken + h.connectionID = 0 + h.serverID = 0 + h.callbackToken = 0 + h.mu.Unlock() + + if callbackToken != 0 { + outboundTargets.Delete(callbackToken) + } + + // Stop accepting new callbacks and wait for in-flight ones to drain before + // closing the receive buffer they feed. + for { + h.mu.Lock() + if h.activeCallbacks == 0 { + h.mu.Unlock() + break + } + h.mu.Unlock() + runtime.Gosched() + } + + if connID != 0 { + h.lib.connectionClose(connID) + } + if serverID != 0 { + h.lib.hostShutdown(serverID) + // libuv may restore a previously saved SIGCHLD action while tearing down + // its final child watcher, so repair the process-wide handler again after + // shutdown before Go reaps another os/exec child. + rearmForeignSignalHandlers(h.lib.handle) + } + h.recv.Close() +} + +// hostWriter adapts Host into the io.WriteCloser jsonrpc2 writes request frames to. +type hostWriter struct{ h *Host } + +func (w hostWriter) Write(p []byte) (int, error) { return w.h.writeFrame(p) } + +func (w hostWriter) Close() error { + w.h.Dispose() + return nil +} diff --git a/go/internal/ffihost/ffihost_test.go b/go/internal/ffihost/ffihost_test.go new file mode 100644 index 0000000000..bc588fa6a9 --- /dev/null +++ b/go/internal/ffihost/ffihost_test.go @@ -0,0 +1,98 @@ +//go:build copilot_inprocess && (darwin || linux || windows) + +package ffihost + +import ( + "encoding/json" + "sync/atomic" + "testing" + "time" + "unsafe" +) + +func TestDisposeUnregistersOutboundTarget(t *testing.T) { + token := uintptr(nextOutboundToken.Add(1)) + host := &Host{ + recv: newReceiveBuffer(), + callbackToken: token, + } + outboundTargets.Store(token, host) + + host.Dispose() + + if _, ok := outboundTargets.Load(token); ok { + t.Fatal("Expected disposed host to be removed from outbound callback registry") + } +} + +func TestBuildArgvAppendsManagedOptions(t *testing.T) { + host := &Host{ + cliEntrypoint: "copilot", + args: []string{"--log-level", "debug", "--remote"}, + } + + var argv []string + if err := json.Unmarshal(host.buildArgv(), &argv); err != nil { + t.Fatal(err) + } + + expected := []string{"copilot", "--embedded-host", "--no-auto-update", "--log-level", "debug", "--remote"} + if len(argv) != len(expected) { + t.Fatalf("Expected %d arguments, got %d: %v", len(expected), len(argv), argv) + } + for i := range expected { + if argv[i] != expected[i] { + t.Fatalf("Expected argument %d to be %q, got %q", i, expected[i], argv[i]) + } + } +} + +func TestDisposeWaitsForStartBeforeShuttingDown(t *testing.T) { + started := make(chan struct{}) + releaseStart := make(chan struct{}) + startDone := make(chan error, 1) + disposeDone := make(chan struct{}) + var shutdownID atomic.Uint32 + + host := &Host{ + lib: &ffiLibrary{ + hostStart: func(_ unsafe.Pointer, _ uintptr, _ unsafe.Pointer, _ uintptr) uint32 { + close(started) + <-releaseStart + return 41 + }, + hostShutdown: func(serverID uint32) bool { + shutdownID.Store(serverID) + return true + }, + connectionOpen: func(_ uint32, _ uintptr, _ uintptr, _ unsafe.Pointer, _ uintptr, _ unsafe.Pointer, _ uintptr, _ unsafe.Pointer, _ uintptr) uint32 { + return 42 + }, + connectionClose: func(_ uint32) bool { return true }, + }, + recv: newReceiveBuffer(), + } + + go func() { startDone <- host.Start() }() + <-started + go func() { + host.Dispose() + close(disposeDone) + }() + + select { + case <-disposeDone: + t.Fatal("Dispose returned before native startup completed") + case <-time.After(20 * time.Millisecond): + } + + close(releaseStart) + if err := <-startDone; err != nil { + t.Fatal(err) + } + <-disposeDone + + if got := shutdownID.Load(); got != 41 { + t.Fatalf("Expected shutdown of server 41, got %d", got) + } +} diff --git a/go/internal/ffihost/loader_other.go b/go/internal/ffihost/loader_other.go new file mode 100644 index 0000000000..0b7bcc40b9 --- /dev/null +++ b/go/internal/ffihost/loader_other.go @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +//go:build copilot_inprocess && (darwin || linux) + +package ffihost + +import "github.com/ebitengine/purego" + +// openLibrary loads the shared library at path and returns an opaque handle. +// RTLD_NOW surfaces any load problem here (eager binding) rather than at first +// call, matching the .NET/Python hosts; RTLD_LOCAL keeps the runtime's symbols +// private to this handle. +func openLibrary(path string) (uintptr, error) { + return purego.Dlopen(path, purego.RTLD_NOW|purego.RTLD_LOCAL) +} diff --git a/go/internal/ffihost/loader_windows.go b/go/internal/ffihost/loader_windows.go new file mode 100644 index 0000000000..4ac2789f98 --- /dev/null +++ b/go/internal/ffihost/loader_windows.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +//go:build copilot_inprocess && windows + +package ffihost + +import "syscall" + +// openLibrary loads the DLL at path and returns its module handle. purego's +// RegisterLibFunc resolves exports from this handle via GetProcAddress, so the +// standard-library loader is sufficient and keeps CGO disabled. +func openLibrary(path string) (uintptr, error) { + handle, err := syscall.LoadLibrary(path) + if err != nil { + return 0, err + } + return uintptr(handle), nil +} diff --git a/go/internal/ffihost/resolve.go b/go/internal/ffihost/resolve.go new file mode 100644 index 0000000000..c8d4052322 --- /dev/null +++ b/go/internal/ffihost/resolve.go @@ -0,0 +1,117 @@ +package ffihost + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" +) + +// NaturalLibraryName is the natural platform shared-library file name for the +// runtime cdylib — the `.node` file renamed to what a Rust cdylib would be +// called on this OS. The library is loaded by absolute path, so the on-disk name +// is ours to choose; this matches the flat name the bundler installs next to the +// CLI binary and the name the other SDKs use. +func NaturalLibraryName() string { + switch runtime.GOOS { + case "windows": + return "copilot_runtime.dll" + case "darwin": + return "libcopilot_runtime.dylib" + default: + return "libcopilot_runtime.so" + } +} + +// PrebuildsFolder returns the napi-rs `-` folder name the +// runtime package ships under prebuilds/ (e.g. linux-x64, darwin-arm64, +// win32-x64, including the musl variant on Alpine). Returns "" for unsupported +// platforms. +func PrebuildsFolder() string { + var platform string + switch runtime.GOOS { + case "linux": + if isMusl() { + platform = "linuxmusl" + } else { + platform = "linux" + } + case "darwin": + platform = "darwin" + case "windows": + platform = "win32" + default: + return "" + } + + var arch string + switch runtime.GOARCH { + case "amd64": + arch = "x64" + case "arm64": + arch = "arm64" + default: + return "" + } + return platform + "-" + arch +} + +// ResolveLibraryPath resolves the native runtime library next to the given CLI +// entrypoint. It checks, in order: +// +// 1. The natural platform library name next to the CLI (bundled/flat layout). +// 2. prebuilds//runtime.node next to the CLI (dev/package layout). +// +// It returns an error when neither exists. +func ResolveLibraryPath(cliEntrypoint string) (string, error) { + abs, err := filepath.Abs(cliEntrypoint) + if err != nil { + abs = cliEntrypoint + } + dir := filepath.Dir(abs) + + flat := filepath.Join(dir, NaturalLibraryName()) + if fileExists(flat) { + return flat, nil + } + + if folder := PrebuildsFolder(); folder != "" { + prebuilt := filepath.Join(dir, "prebuilds", folder, "runtime.node") + if fileExists(prebuilt) { + return prebuilt, nil + } + } + + return "", fmt.Errorf( + "in-process FFI runtime library not found next to %q (looked for %q and prebuilds/%s/runtime.node); "+ + "use a runtime package that ships the native library", + abs, NaturalLibraryName(), PrebuildsFolder()) +} + +func fileExists(path string) bool { + info, err := os.Stat(path) + return err == nil && !info.IsDir() +} + +var ( + muslOnce sync.Once + muslResult bool +) + +// isMusl reports whether the current Linux system uses musl libc (e.g. Alpine), +// which ships the runtime under the linuxmusl- prebuilds folder. +func isMusl() bool { + muslOnce.Do(func() { + if runtime.GOOS != "linux" { + return + } + // `ldd --version` prints "musl libc" on musl systems and errors/glibc text + // elsewhere; a best-effort check is enough to pick the prebuilds folder. + out, _ := exec.Command("ldd", "--version").CombinedOutput() + muslResult = strings.Contains(strings.ToLower(string(out)), "musl") + }) + return muslResult +} diff --git a/go/internal/ffihost/resolve_test.go b/go/internal/ffihost/resolve_test.go new file mode 100644 index 0000000000..df3a668dfe --- /dev/null +++ b/go/internal/ffihost/resolve_test.go @@ -0,0 +1,54 @@ +package ffihost + +import ( + "os" + "path/filepath" + "testing" +) + +func TestResolveLibraryPathUsesNaturalLibraryNextToCLI(t *testing.T) { + dir := t.TempDir() + cliPath := filepath.Join(dir, "copilot") + libraryPath := filepath.Join(dir, NaturalLibraryName()) + + for _, path := range []string{cliPath, libraryPath} { + if err := os.WriteFile(path, []byte("test"), 0600); err != nil { + t.Fatalf("WriteFile(%q): %v", path, err) + } + } + + got, err := ResolveLibraryPath(cliPath) + if err != nil { + t.Fatalf("ResolveLibraryPath() error: %v", err) + } + if got != libraryPath { + t.Fatalf("ResolveLibraryPath() = %q, want %q", got, libraryPath) + } +} + +func TestResolveLibraryPathFallsBackToPrebuilds(t *testing.T) { + folder := PrebuildsFolder() + if folder == "" { + t.Skip("unsupported platform") + } + + dir := t.TempDir() + cliPath := filepath.Join(dir, "copilot") + libraryPath := filepath.Join(dir, "prebuilds", folder, "runtime.node") + if err := os.MkdirAll(filepath.Dir(libraryPath), 0755); err != nil { + t.Fatalf("MkdirAll(): %v", err) + } + for _, path := range []string{cliPath, libraryPath} { + if err := os.WriteFile(path, []byte("test"), 0600); err != nil { + t.Fatalf("WriteFile(%q): %v", path, err) + } + } + + got, err := ResolveLibraryPath(cliPath) + if err != nil { + t.Fatalf("ResolveLibraryPath() error: %v", err) + } + if got != libraryPath { + t.Fatalf("ResolveLibraryPath() = %q, want %q", got, libraryPath) + } +} diff --git a/go/internal/ffihost/sigonstack_darwin.go b/go/internal/ffihost/sigonstack_darwin.go new file mode 100644 index 0000000000..2e7d2a99b7 --- /dev/null +++ b/go/internal/ffihost/sigonstack_darwin.go @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT + +//go:build copilot_inprocess && darwin + +package ffihost + +import ( + "encoding/binary" + "unsafe" + + "github.com/ebitengine/purego" +) + +// Darwin `struct sigaction` layout (16 bytes, little-endian on amd64/arm64): +// +// offset 0: union __sigaction_u sa_handler/sa_sigaction (8 bytes, pointer) +// offset 8: sigset_t sa_mask (4 bytes, uint32) +// offset 12: int sa_flags (4 bytes) +const ( + darwinSigactionSize = 16 + darwinFlagsOffset = 12 + saOnStack = 0x0001 // SA_ONSTACK on Darwin + sigDfl = 0 // SIG_DFL + sigIgn = 1 // SIG_IGN + maxSignal = 31 // NSIG-1 on Darwin +) + +// rearmForeignSignalHandlers re-adds the SA_ONSTACK flag to any signal handler +// installed by the native runtime (libnode/libuv, loaded via dlopen) that +// omitted it. The Go runtime aborts with "non-Go code set up signal handler +// without SA_ONSTACK flag" when such a signal (notably SIGCHLD, signal 20 on +// Darwin) is delivered while a Go-managed child process is reaped. libuv +// installs a SIGCHLD handler without SA_ONSTACK, which poisons every subsequent +// os/exec child reaped by Go in the same process (enforced by the Go runtime on +// both macOS and Linux; the Linux variant lives in sigonstack_linux.go). +// +// We preserve each foreign handler and merely OR in SA_ONSTACK, so libuv's child +// watching keeps working while the Go runtime stays happy. Handlers left at +// SIG_DFL/SIG_IGN and Go's own handlers (which already carry SA_ONSTACK) are +// untouched. Best-effort: any failure is silently ignored, since the worst case +// is the pre-existing crash. +func rearmForeignSignalHandlers(_ uintptr) { + handle, err := purego.Dlopen("/usr/lib/libSystem.B.dylib", purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil || handle == 0 { + return + } + + var sigaction func(sig int32, act, oact unsafe.Pointer) int32 + if !bindSigaction(handle, &sigaction) { + return + } + + for sig := int32(1); sig <= maxSignal; sig++ { + var cur [darwinSigactionSize]byte + if sigaction(sig, nil, unsafe.Pointer(&cur[0])) != 0 { + continue + } + handler := binary.LittleEndian.Uint64(cur[0:8]) + if handler == sigDfl || handler == sigIgn { + continue + } + flags := binary.LittleEndian.Uint32(cur[darwinFlagsOffset : darwinFlagsOffset+4]) + if flags&saOnStack != 0 { + continue + } + binary.LittleEndian.PutUint32(cur[darwinFlagsOffset:darwinFlagsOffset+4], flags|saOnStack) + sigaction(sig, unsafe.Pointer(&cur[0]), nil) + } +} + +// bindSigaction resolves libc's sigaction into fn, converting the panic +// RegisterLibFunc raises on a missing symbol into a false return. +func bindSigaction(handle uintptr, fn *func(sig int32, act, oact unsafe.Pointer) int32) (ok bool) { + defer func() { + if recover() != nil { + ok = false + } + }() + purego.RegisterLibFunc(fn, handle, "sigaction") + return true +} diff --git a/go/internal/ffihost/sigonstack_linux.go b/go/internal/ffihost/sigonstack_linux.go new file mode 100644 index 0000000000..668cf67055 --- /dev/null +++ b/go/internal/ffihost/sigonstack_linux.go @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT + +//go:build copilot_inprocess && linux + +package ffihost + +import ( + "syscall" + "unsafe" +) + +const ( + linuxSaOnStack = 0x08000000 + linuxSigDfl = 0 + linuxSigIgn = 1 + linuxMaxSignal = 31 +) + +// linuxSigaction matches the kernel rt_sigaction ABI used by the Go runtime on +// Linux amd64 and arm64. +type linuxSigaction struct { + handler uintptr + flags uint64 + restorer uintptr + mask uint64 +} + +// rearmForeignSignalHandlers re-adds the SA_ONSTACK flag to any signal handler +// installed by the native runtime (libnode/libuv, loaded via dlopen) that +// omitted it. The Go runtime aborts with "non-Go code set up signal handler +// without SA_ONSTACK flag" when such a signal (notably SIGCHLD, signal 17 on +// Linux) is delivered while a Go-managed child process is reaped. libuv installs +// a SIGCHLD handler without SA_ONSTACK, which poisons every subsequent os/exec +// child reaped by Go in the same process. +// +// We preserve each foreign handler and merely OR in SA_ONSTACK, so libuv's child +// watching keeps working while the Go runtime stays happy. Handlers left at +// SIG_DFL/SIG_IGN and Go's own handlers (which already carry SA_ONSTACK) are +// untouched. Best-effort: any failure is silently ignored, since the worst case +// is the pre-existing crash. +func rearmForeignSignalHandlers(_ uintptr) { + for sig := 1; sig <= linuxMaxSignal; sig++ { + var action linuxSigaction + if !linuxGetSigaction(sig, &action) { + continue + } + if action.handler == linuxSigDfl || action.handler == linuxSigIgn { + continue + } + if action.flags&linuxSaOnStack != 0 { + continue + } + action.flags |= linuxSaOnStack + linuxSetSigaction(sig, &action) + } +} + +func linuxGetSigaction(signal int, action *linuxSigaction) bool { + _, _, errno := syscall.RawSyscall6( + syscall.SYS_RT_SIGACTION, + uintptr(signal), + 0, + uintptr(unsafe.Pointer(action)), + unsafe.Sizeof(action.mask), + 0, + 0, + ) + return errno == 0 +} + +func linuxSetSigaction(signal int, action *linuxSigaction) bool { + _, _, errno := syscall.RawSyscall6( + syscall.SYS_RT_SIGACTION, + uintptr(signal), + uintptr(unsafe.Pointer(action)), + 0, + unsafe.Sizeof(action.mask), + 0, + 0, + ) + return errno == 0 +} diff --git a/go/internal/ffihost/sigonstack_linux_test.go b/go/internal/ffihost/sigonstack_linux_test.go new file mode 100644 index 0000000000..3a382385f2 --- /dev/null +++ b/go/internal/ffihost/sigonstack_linux_test.go @@ -0,0 +1,38 @@ +//go:build copilot_inprocess && linux + +package ffihost + +import ( + "os" + "os/signal" + "syscall" + "testing" +) + +func TestRearmForeignSignalHandlersAddsOnStack(t *testing.T) { + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGUSR1) + defer signal.Stop(signals) + + var original linuxSigaction + if !linuxGetSigaction(int(syscall.SIGUSR1), &original) { + t.Fatal("failed to read SIGUSR1 action") + } + defer linuxSetSigaction(int(syscall.SIGUSR1), &original) + + withoutOnStack := original + withoutOnStack.flags &^= linuxSaOnStack + if !linuxSetSigaction(int(syscall.SIGUSR1), &withoutOnStack) { + t.Fatal("failed to clear SA_ONSTACK") + } + + rearmForeignSignalHandlers(0) + + var rearmed linuxSigaction + if !linuxGetSigaction(int(syscall.SIGUSR1), &rearmed) { + t.Fatal("failed to read rearmed SIGUSR1 action") + } + if rearmed.flags&linuxSaOnStack == 0 { + t.Fatal("SA_ONSTACK was not restored") + } +} diff --git a/go/internal/ffihost/sigonstack_other.go b/go/internal/ffihost/sigonstack_other.go new file mode 100644 index 0000000000..9da78255f6 --- /dev/null +++ b/go/internal/ffihost/sigonstack_other.go @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT + +//go:build copilot_inprocess && windows + +package ffihost + +// rearmForeignSignalHandlers is a no-op on platforms other than darwin and +// linux. Only those Unix platforms deliver the SA_ONSTACK-less SIGCHLD handler +// (installed by libuv) that the Go runtime rejects; Windows is unaffected. +func rearmForeignSignalHandlers(_ uintptr) {} diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 5343c518c4..30ae6ff271 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -1784,6 +1784,8 @@ type DiscoveredCanvas struct { ExtensionID string `json:"extensionId"` // Owning extension display name, when available ExtensionName *string `json:"extensionName,omitempty"` + // Host-local PNG path for the canvas icon, when supplied + Icon *string `json:"icon,omitempty"` // JSON Schema for canvas open input InputSchema any `json:"inputSchema,omitempty"` } @@ -2461,6 +2463,26 @@ type HistoryTruncateResult struct { EventsRemoved int64 `json:"eventsRemoved"` } +// Runtime-owned wire payload for a server-to-client hook callback invocation. +// Experimental: HookInvokeRequest is part of an experimental API and may change or be +// 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"` +} + +// Optional output returned by an SDK callback hook. +// Experimental: HookInvokeResponse is part of an experimental API and may change or be +// removed. +// Internal: HookInvokeResponse is an internal SDK API and is not part of the public surface. +type HookInvokeResponse struct { + Output any `json:"output,omitempty"` +} + // 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. @@ -3595,15 +3617,172 @@ type MCPRemoveGitHubResult struct { Removed bool `json:"removed"` } -// Server name and opaque configuration for an individual MCP server restart. +// An MCP resource descriptor (spec `Resource`): URI, name, and optional title, description, +// MIME type, size, icons, annotations, and metadata. Server-provided fields outside the +// standard descriptor shape are exposed under `additionalProperties`. +// Experimental: MCPResource is part of an experimental API and may change or be removed. +type MCPResource struct { + // Server-provided non-standard descriptor fields preserved from the MCP response + AdditionalProperties map[string]any `json:"additionalProperties,omitzero"` + // Model/client annotations associated with this resource + Annotations *MCPResourceAnnotations `json:"annotations,omitempty"` + // Optional description of what this resource represents + Description *string `json:"description,omitempty"` + // Icons associated with this resource + Icons []MCPResourceIcon `json:"icons,omitzero"` + // Resource-level metadata + Meta map[string]any `json:"_meta,omitzero"` + // MIME type of the resource, if known + MIMEType *string `json:"mimeType,omitempty"` + // The programmatic name of the resource + Name string `json:"name"` + // Resource size in bytes, when known + Size *int64 `json:"size,omitempty"` + // Optional human-readable display title + Title *string `json:"title,omitempty"` + // The resource URI (e.g. ui://... or file:///...) + URI string `json:"uri"` +} + +// Standard MCP resource annotations plus preserved non-standard annotation fields. +// Experimental: MCPResourceAnnotations is part of an experimental API and may change or be +// removed. +type MCPResourceAnnotations struct { + // Server-provided non-standard annotation fields preserved from the MCP response + AdditionalProperties map[string]any `json:"additionalProperties,omitzero"` + // Intended audience roles for this resource + Audience []string `json:"audience,omitzero"` + // Last-modified timestamp hint + LastModified *string `json:"lastModified,omitempty"` + // Priority hint for model/client use + Priority *float64 `json:"priority,omitempty"` +} + +// MCP resource content with URI, optional MIME type, text or base64 blob, and resource +// metadata. +// Experimental: MCPResourceContent is part of an experimental API and may change or be +// removed. +type MCPResourceContent struct { + // Base64-encoded binary content + Blob *string `json:"blob,omitempty"` + // Resource-level metadata (CSP, permissions, etc.) + Meta map[string]any `json:"_meta,omitzero"` + // MIME type of the content + MIMEType *string `json:"mimeType,omitempty"` + // Text content (e.g. HTML) + Text *string `json:"text,omitempty"` + // The resource URI + URI string `json:"uri"` +} + +// A resource icon descriptor plus preserved non-standard icon fields. +// Experimental: MCPResourceIcon is part of an experimental API and may change or be removed. +type MCPResourceIcon struct { + // Server-provided non-standard icon fields preserved from the MCP response + AdditionalProperties map[string]any `json:"additionalProperties,omitzero"` + // Icon MIME type, when known + MIMEType *string `json:"mimeType,omitempty"` + // Icon sizes hint + Sizes *string `json:"sizes,omitempty"` + // Icon URI + Src string `json:"src"` + // Theme hint for this icon + Theme *string `json:"theme,omitempty"` +} + +// MCP server whose resources to enumerate. +// Experimental: MCPResourcesListRequest is part of an experimental API and may change or be +// removed. +type MCPResourcesListRequest struct { + // Opaque MCP pagination cursor from a prior `nextCursor` value + Cursor *string `json:"cursor,omitempty"` + // Name of the MCP server whose resources to enumerate + ServerName string `json:"serverName"` +} + +// One page of resources advertised by the named MCP server. +// Experimental: MCPResourcesListResult is part of an experimental API and may change or be +// removed. +type MCPResourcesListResult struct { + // Opaque cursor for the next page, if the server has more resources + NextCursor *string `json:"nextCursor,omitempty"` + // Resources advertised by the server (proxied MCP `resources/list`) + Resources []MCPResource `json:"resources"` +} + +// MCP server whose resource templates to enumerate. +// Experimental: MCPResourcesListTemplatesRequest is part of an experimental API and may +// change or be removed. +type MCPResourcesListTemplatesRequest struct { + // Opaque MCP pagination cursor from a prior `nextCursor` value + Cursor *string `json:"cursor,omitempty"` + // Name of the MCP server whose resource templates to enumerate + ServerName string `json:"serverName"` +} + +// One page of resource templates advertised by the named MCP server. +// Experimental: MCPResourcesListTemplatesResult is part of an experimental API and may +// change or be removed. +type MCPResourcesListTemplatesResult struct { + // Opaque cursor for the next page, if the server has more resource templates + NextCursor *string `json:"nextCursor,omitempty"` + // Resource templates advertised by the server (proxied MCP `resources/templates/list`) + ResourceTemplates []MCPResourceTemplate `json:"resourceTemplates"` +} + +// MCP server and resource URI to fetch. +// Experimental: MCPResourcesReadRequest is part of an experimental API and may change or be +// removed. +type MCPResourcesReadRequest struct { + // Name of the MCP server hosting the resource + ServerName string `json:"serverName"` + // Resource URI + URI string `json:"uri"` +} + +// Resource contents returned by the MCP server. +// Experimental: MCPResourcesReadResult is part of an experimental API and may change or be +// removed. +type MCPResourcesReadResult struct { + // Resource contents returned by the server + Contents []MCPResourceContent `json:"contents"` +} + +// An MCP resource template descriptor (spec `ResourceTemplate`): an RFC 6570 URI template, +// name, and optional title, description, MIME type, icons, annotations, and metadata. +// Server-provided fields outside the standard descriptor shape are exposed under +// `additionalProperties`. +// Experimental: MCPResourceTemplate is part of an experimental API and may change or be +// removed. +type MCPResourceTemplate struct { + // Server-provided non-standard descriptor fields preserved from the MCP response + AdditionalProperties map[string]any `json:"additionalProperties,omitzero"` + // Model/client annotations associated with this template + Annotations *MCPResourceAnnotations `json:"annotations,omitempty"` + // Optional description of what this template is for + Description *string `json:"description,omitempty"` + // Icons associated with resources matching this template + Icons []MCPResourceIcon `json:"icons,omitzero"` + // Resource-template-level metadata + Meta map[string]any `json:"_meta,omitzero"` + // MIME type for resources matching this template, if uniform + MIMEType *string `json:"mimeType,omitempty"` + // The programmatic name of the resource template + Name string `json:"name"` + // Optional human-readable display title + Title *string `json:"title,omitempty"` + // An RFC 6570 URI template for constructing resource URIs + URITemplate string `json:"uriTemplate"` +} + +// Server name and optional replacement configuration for an individual MCP server restart. +// Omit `config` for a config-free restart-by-name of an already-configured server. // Experimental: MCPRestartServerRequest is part of an experimental API and may change or be // removed. type MCPRestartServerRequest struct { - // Opaque server configuration (MCPServerConfig). Marked internal: an in-process runtime - // shape supplied only by in-process CLI callers. - // Internal: Config is part of the SDK's internal API surface and is not intended for - // external use. - Config any `json:"config"` + // Replacement MCP server configuration (stdio process or remote HTTP/SSE). Omit to restart + // the server with its already-registered configuration (config-free restart-by-name). + Config MCPServerConfig `json:"config,omitempty"` // Name of the MCP server to restart ServerName string `json:"serverName"` } @@ -3791,15 +3970,12 @@ type MCPSetEnvValueModeResult struct { Mode MCPSetEnvValueModeDetails `json:"mode"` } -// Server name and opaque configuration for an individual MCP server start. +// Server name and configuration for an individual MCP server start. // Experimental: MCPStartServerRequest is part of an experimental API and may change or be // removed. type MCPStartServerRequest struct { - // Opaque server configuration (MCPServerConfig). Marked internal: an in-process runtime - // shape supplied only by in-process CLI callers. - // Internal: Config is part of the SDK's internal API surface and is not intended for - // external use. - Config any `json:"config"` + // MCP server configuration (stdio process or remote HTTP/SSE) + Config MCPServerConfig `json:"config"` // Name of the MCP server to start ServerName string `json:"serverName"` } @@ -3822,13 +3998,27 @@ type MCPStopServerRequest struct { ServerName string `json:"serverName"` } -// MCP tool metadata with tool name and optional description. +// MCP tool metadata with tool name, optional description, and normalized MCP Apps discovery +// metadata. // Experimental: MCPTools is part of an experimental API and may change or be removed. type MCPTools struct { // Tool description, when provided. Description *string `json:"description,omitempty"` // Tool name. Name string `json:"name"` + // Normalized MCP Apps discovery metadata. An empty object indicates that a valid `_meta.ui` + // block was present without recognized fields. + UI *MCPToolUI `json:"ui,omitempty"` +} + +// Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block. +// Experimental: MCPToolUI is part of an experimental API and may change or be removed. +type MCPToolUI struct { + // URI of the tool's MCP App resource, typically a `ui://` resource identifier. Use + // `session.mcp.resources.read` to fetch its HTML and resource metadata. + ResourceURI *string `json:"resourceUri,omitempty"` + // Tool visibility advertised by the server. When absent, MCP Apps defaults apply. + Visibility []MCPToolUIVisibility `json:"visibility,omitzero"` } // Server name identifying the external client to remove. @@ -3952,7 +4142,10 @@ type MetadataRecordContextChangeRequest struct { type MetadataRecordContextChangeResult struct { } -// Absolute path to set as the session's new working directory. +// Absolute path to set as the session's new working directory. For local sessions the path +// must be absolute and exist on disk: it is validated before any session state changes, and +// a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote +// sessions record the path as-is. // Experimental: MetadataSetWorkingDirectoryRequest is part of an experimental API and may // change or be removed. type MetadataSetWorkingDirectoryRequest struct { @@ -3963,9 +4156,13 @@ type MetadataSetWorkingDirectoryRequest struct { } // Update the session's working directory. Used by the host when the user explicitly changes -// cwd (e.g., the `/cd` slash command). The host is responsible for `process.chdir` and any -// related side-effects (file index, etc.); this method only updates the session's own -// recorded path. +// cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects +// (file index, etc.); it does NOT change the process working directory (a session's cwd is +// per-session, not process-global). For local sessions the runtime validates the target +// first (an absolute path that exists on disk) and re-bases the permission primary +// directory; a rejected validation fails the call before anything is mutated, persisted, or +// emitted. Location-scoped permission rules are then re-keyed to the new directory +// (best-effort). Remote sessions only record the path. // Experimental: MetadataSetWorkingDirectoryResult is part of an experimental API and may // change or be removed. type MetadataSetWorkingDirectoryResult struct { @@ -4035,10 +4232,30 @@ type ModelBilling struct { DiscountPercent *int32 `json:"discountPercent,omitempty"` // Billing cost multiplier relative to the base rate Multiplier *float64 `json:"multiplier,omitempty"` + // Active server-driven promotion for this model, if any. Present when the model is being + // promoted with a time-boxed discount. + Promo *ModelBillingPromo `json:"promo,omitempty"` // Token-level pricing information for this model TokenPrices *ModelBillingTokenPrices `json:"tokenPrices,omitempty"` } +// Active server-driven promotion for a model, including its discount and expiry. +// Experimental: ModelBillingPromo is part of an experimental API and may change or be +// removed. +type ModelBillingPromo struct { + // Percentage discount (0-100) applied while the promotion is active. May be fractional. + DiscountPercent *float64 `json:"discountPercent,omitempty"` + // UTC ISO 8601 timestamp marking when the promotion ends. Always present: the API only + // surfaces a promo whose expiry parses and is in the future. Consumers should treat a past + // value as expired. + EndsAt string `json:"endsAt"` + // Stable identifier for the promotion campaign. + ID *string `json:"id,omitempty"` + // Human-readable promotion message. Does not include the expiry timestamp; consumers may + // format endsAt and append it. + Message *string `json:"message,omitempty"` +} + // Token-level pricing information for this model // Experimental: ModelBillingTokenPrices is part of an experimental API and may change or be // removed. @@ -4259,6 +4476,8 @@ type ModelSwitchToRequest struct { ReasoningEffort *string `json:"reasoningEffort,omitempty"` // Reasoning summary mode to request for supported model clients ReasoningSummary *ReasoningSummary `json:"reasoningSummary,omitempty"` + // Output verbosity level to request for supported models + Verbosity *Verbosity `json:"verbosity,omitempty"` } // The model identifier active on the session after the switch. @@ -4355,6 +4574,8 @@ type OpenCanvasInstance struct { ExtensionID string `json:"extensionId"` // Owning extension display name, when available ExtensionName *string `json:"extensionName,omitempty"` + // Host-local PNG path for the canvas icon, when supplied + Icon *string `json:"icon,omitempty"` // Input supplied when the instance was opened Input any `json:"input,omitempty"` // Stable caller-supplied canvas instance identifier @@ -5689,7 +5910,7 @@ type PluginsInstallRequest struct { WorkingDirectory *string `json:"workingDirectory,omitempty"` } -// Marketplace source to register. +// Marketplace source and optional working directory for relative-path resolution. // Experimental: PluginsMarketplacesAddRequest is part of an experimental API and may change // or be removed. type PluginsMarketplacesAddRequest struct { @@ -5698,6 +5919,9 @@ type PluginsMarketplacesAddRequest struct { // (user@host:path), or a local path. The marketplace's own name (from its manifest) is used // as the registration key. Source string `json:"source"` + // Working directory used to resolve relative local paths in `source`. Defaults to the + // server's current working directory. + WorkingDirectory *string `json:"workingDirectory,omitempty"` } // Name of the marketplace whose plugin catalog to fetch. @@ -5736,6 +5960,10 @@ type PluginsReloadRequest struct { DeferRepoHooks *bool `json:"deferRepoHooks,omitempty"` // Re-run custom-agent discovery after refreshing plugins. Defaults to true. ReloadCustomAgents *bool `json:"reloadCustomAgents,omitempty"` + // Re-discover and relaunch subprocess extensions (including plugin-shipped extensions) + // after refreshing plugins. Defaults to true. Has no effect when the session has no active + // extension controller (e.g. extensions were not requested for the session). + ReloadExtensions *bool `json:"reloadExtensions,omitempty"` // Re-load user, plugin, and (subject to `deferRepoHooks`) repo hooks. Defaults to true. Has // no effect when the host has not registered a hook reloader (e.g. remote sessions). ReloadHooks *bool `json:"reloadHooks,omitempty"` @@ -6422,14 +6650,16 @@ type RegisterEventInterestParams struct { // The event type the consumer wants the runtime to treat as 'observed' for // behavior-switching gating. Some runtime code paths inspect whether any consumer is // interested in a specific event type and choose a different implementation accordingly - // (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates the full - // interactive OAuth flow to the consumer; when no interest is registered the runtime - // installs a browserless fallback that silently reuses cached tokens). SDK clients that - // long-poll events do NOT automatically appear as listeners to these gating checks — they - // must explicitly call `registerInterest` for each event type they want the runtime to - // count as having a consumer. Multiple registrations for the same event type from the same - // or different consumers are tracked independently and must each be released. See: - // `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, + // (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates interactive + // OAuth token acquisition to the consumer via `mcp.oauth_required` events; when no interest + // is registered the runtime still attempts non-interactive reconnect from cached or + // refreshable tokens, and only marks the server `needs-auth` if usable credentials are + // unavailable — it does not open a browser or start interactive OAuth without a consumer). + // SDK clients that long-poll events do NOT automatically appear as listeners to these + // gating checks — they must explicitly call `registerInterest` for each event type they + // want the runtime to count as having a consumer. Multiple registrations for the same event + // type from the same or different consumers are tracked independently and must each be + // released. See: `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, // `session_limits_exhausted.requested`, `user_input.requested`, `elicitation.requested`, // `command.queued`, `exit_plan_mode.requested`. EventType string `json:"eventType"` @@ -6541,6 +6771,12 @@ func (r RawRemoteControlStatusData) State() RemoteControlStatusState { type RemoteControlStatusActive struct { // Session id remote control is pointed at. AttachedSessionID string `json:"attachedSessionId"` + // True while a read-only/session-sync export is deferred, awaiting the first `user.message` + // before its MC session exists. Marked internal: this field is excluded from the public SDK + // surface and is populated only on the CLI in-process path. + // Internal: AwaitingFirstMessage is part of the SDK's internal API surface and is not + // intended for external use. + AwaitingFirstMessage *bool `json:"awaitingFirstMessage,omitempty"` // MC frontend URL for this session, when known. FrontendURL *string `json:"frontendUrl,omitempty"` // Whether the MC session may steer this session. @@ -6859,6 +7095,73 @@ type SendAttachmentsToMessageParams struct { InstanceID *string `json:"instanceId,omitempty"` } +// A single user message to append to the session as part of a `session.sendMessages` turn +// Experimental: SendMessageItem is part of an experimental API and may change or be removed. +type SendMessageItem struct { + // Optional attachments (files, directories, selections, blobs, GitHub references) to + // include with this message + Attachments []Attachment `json:"attachments,omitzero"` + // If false, this message will not trigger a Premium Request Unit charge. User messages + // default to billable. + // Internal: Billable is part of the SDK's internal API surface and is not intended for + // external use. + Billable *bool `json:"billable,omitempty"` + // If provided, this is shown in the timeline instead of `prompt` + DisplayPrompt *string `json:"displayPrompt,omitempty"` + // The user message text + Prompt string `json:"prompt"` + // If set, the request will fail if the named tool is not available when this message is + // among the user messages at the start of the current exchange + RequiredTool *string `json:"requiredTool,omitempty"` + // Optional provenance tag copied to the resulting user.message event. Must match one of + // three forms: the literal `system`, `command-` for messages originating from a + // command (e.g. slash command, Mission Control command), or `schedule-` for + // messages originating from a scheduled job. + // Internal: Source is part of the SDK's internal API surface and is not intended for + // external use. + Source *string `json:"source,omitempty"` +} + +// Parameters for sending zero or more user messages to the session in a single turn. +// Remote-backed (Mission Control) sessions do not support this method and will return an +// error. +// Experimental: SendMessagesRequest is part of an experimental API and may change or be +// removed. +type SendMessagesRequest struct { + // The UI mode the agent was in when these messages were sent. Defaults to the session's + // current mode. + AgentMode *SendAgentMode `json:"agentMode,omitempty"` + // The user messages to append to the conversation, in order. May be empty, in which case a + // single turn runs over the existing history with no new user message. + Messages []SendMessageItem `json:"messages"` + // How to deliver the messages. `enqueue` (default) appends to the message queue. + // `immediate` interjects during an in-progress turn. + Mode *SendMode `json:"mode,omitempty"` + // If true, adds the messages to the front of the queue instead of the end + Prepend *bool `json:"prepend,omitempty"` + // Custom HTTP headers to include in outbound model requests for this turn. Merged with + // session-level provider headers; per-turn headers augment and overwrite session-level + // headers with the same key. + RequestHeaders map[string]string `json:"requestHeaders,omitzero"` + // W3C Trace Context traceparent header for distributed tracing of this agent turn + Traceparent *string `json:"traceparent,omitempty"` + // W3C Trace Context tracestate header for distributed tracing + Tracestate *string `json:"tracestate,omitempty"` + // If true, await completion of the agentic loop for this turn before returning. Defaults to + // false (fire-and-forget). When true, the result still contains the same `messageIds`; the + // caller can rely on the agent having processed the messages before the call resolves. + Wait *bool `json:"wait,omitempty"` +} + +// Result of sending zero or more user messages +// Experimental: SendMessagesResult is part of an experimental API and may change or be +// removed. +type SendMessagesResult struct { + // Unique identifiers assigned to the messages, one per provided message in order. Empty + // when no messages were provided. + MessageIDs []string `json:"messageIds"` +} + // Parameters for sending a user message to the session // Experimental: SendRequest is part of an experimental API and may change or be removed. type SendRequest struct { @@ -7689,10 +7992,21 @@ type SessionModelList struct { // (CAPI) models and any registry BYOK models; a BYOK model appears under its // provider-qualified selection id (`provider/id`). List []any `json:"list"` + // Cost categories for the full CAPI catalog, including picker-disabled models that Auto may + // select. Metadata only; entries absent from `list` are not manually selectable. + ModelPriceCategories []SessionModelPriceCategory `json:"modelPriceCategories,omitzero"` // Per-quota snapshots returned alongside the model list, keyed by quota type. QuotaSnapshots map[string]any `json:"quotaSnapshots,omitzero"` } +// Cost-category metadata for a CAPI model. +// Experimental: SessionModelPriceCategory is part of an experimental API and may change or +// be removed. +type SessionModelPriceCategory struct { + ID string `json:"id"` + PriceCategory ModelPickerPriceCategory `json:"priceCategory"` +} + // Experimental: SessionModeSetResult is part of an experimental API and may change or be // removed. type SessionModeSetResult struct { @@ -7751,6 +8065,8 @@ type SessionOpenOptions struct { // surface is experimental. // Experimental: EnableCitations is part of an experimental API and may change or be removed. EnableCitations *bool `json:"enableCitations,omitempty"` + // Opt-in: self-fetch and enforce enterprise managed settings at session bootstrap. + EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` // Whether on-demand custom instruction discovery is enabled. EnableOnDemandInstructionDiscovery *bool `json:"enableOnDemandInstructionDiscovery,omitempty"` // Whether shell-script safety heuristics are enabled. @@ -7776,6 +8092,10 @@ type SessionOpenOptions struct { ExpAssignments any `json:"expAssignments,omitempty"` // Feature-flag values resolved by the host. FeatureFlags map[string]bool `json:"featureFlags,omitzero"` + // 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. + IncludedBuiltinAgents []string `json:"includedBuiltinAgents,omitzero"` // Installed plugins visible to the session. InstalledPlugins []InstalledPlugin `json:"installedPlugins,omitzero"` // Stable integration identifier for analytics. @@ -7821,8 +8141,6 @@ type SessionOpenOptions struct { RunningInInteractiveMode *bool `json:"runningInInteractiveMode,omitempty"` // Resolved sandbox configuration. SandboxConfig *SandboxConfig `json:"sandboxConfig,omitempty"` - // Opt-in: self-fetch enterprise managed settings at session bootstrap. - SelfFetchManagedSettings *bool `json:"selfFetchManagedSettings,omitempty"` // Capabilities enabled for this session. SessionCapabilities []SessionCapability `json:"sessionCapabilities,omitzero"` // Optional stable session identifier to use for a new session. @@ -7839,6 +8157,8 @@ type SessionOpenOptions struct { SkipCustomInstructions *bool `json:"skipCustomInstructions,omitempty"` // Optional trajectory output file path. TrajectoryFile *string `json:"trajectoryFile,omitempty"` + // Initial output verbosity level for supported models. + Verbosity *Verbosity `json:"verbosity,omitempty"` // Working directory to anchor the session. WorkingDirectory *string `json:"workingDirectory,omitempty"` // Pre-resolved working-directory context for session startup. @@ -7956,6 +8276,15 @@ type SessionsOpenHandoff struct { // Remote session metadata for the session to hand off (typically obtained from // `sessions.list` with `source: "remote"`). Metadata RemoteSessionMetadataValue `json:"metadata"` + // In-process confirmation callback `(request) => boolean | Promise` invoked when + // the handoff needs the caller to confirm a non-fatal blocker (e.g. a repository mismatch + // between the current working directory and the remote session). Returning `true` proceeds + // with the handoff; returning `false` (or omitting the callback) aborts it. Marked internal + // because a function reference cannot cross the JSON-RPC boundary, for the same reasons as + // `onProgress`. + // Internal: OnConfirm is part of the SDK's internal API surface and is not intended for + // external use. + OnConfirm any `json:"onConfirm,omitempty"` // In-process progress callback `(update) => void` invoked for each handoff step. Marked // internal because a function reference cannot cross the JSON-RPC boundary. The host-side // `handoffSession` is already declared as `AsyncGenerator`; @@ -8702,6 +9031,10 @@ type SessionUpdateOptionsParams struct { ExcludedTools []string `json:"excludedTools,omitzero"` // Map of feature-flag IDs to their boolean enabled state. FeatureFlags map[string]bool `json:"featureFlags,omitzero"` + // 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. Set to null to remove the allowlist restriction. + IncludedBuiltinAgents []string `json:"includedBuiltinAgents,omitzero"` // Full set of installed plugins for the session. Replaces the existing list; the runtime // invalidates the skills cache only when the list materially changes. InstalledPlugins []SessionInstalledPlugin `json:"installedPlugins,omitzero"` @@ -8763,6 +9096,8 @@ type SessionUpdateOptionsParams struct { ToolFilterPrecedence *OptionsUpdateToolFilterPrecedence `json:"toolFilterPrecedence,omitempty"` // Optional path for trajectory output. TrajectoryFile *string `json:"trajectoryFile,omitempty"` + // Output verbosity level for supported models. + Verbosity *Verbosity `json:"verbosity,omitempty"` // Absolute working-directory path for shell tools. WorkingDirectory *string `json:"workingDirectory,omitempty"` } @@ -8771,6 +9106,8 @@ type SessionUpdateOptionsParams struct { // Experimental: SessionUpdateOptionsResult is part of an experimental API and may change or // be removed. type SessionUpdateOptionsResult struct { + // Number of hooks loaded from installed plugins, returned when installedPlugins is updated + PluginHookCount *int64 `json:"pluginHookCount,omitempty"` // Whether the operation succeeded Success bool `json:"success"` } @@ -9088,7 +9425,7 @@ func (r RawSlashCommandInvocationResultData) Kind() SlashCommandInvocationResult } // Slash-command invocation result that submits an agent prompt, with display prompt, -// optional mode, and settings-change flag. +// optional mode, optional user-facing notice, and settings-change flag. // Experimental: SlashCommandAgentPromptResult is part of an experimental API and may change // or be removed. type SlashCommandAgentPromptResult struct { @@ -9096,6 +9433,8 @@ type SlashCommandAgentPromptResult struct { DisplayPrompt string `json:"displayPrompt"` // Optional target session mode for the agent prompt Mode *SessionMode `json:"mode,omitempty"` + // Optional user-facing notice to show before the prompt is submitted + Notice *string `json:"notice,omitempty"` // Prompt to submit to the agent Prompt string `json:"prompt"` // True when the invocation mutated user runtime settings; consumers caching settings should @@ -11064,6 +11403,45 @@ const ( HMACAuthInfoHostHTTPSGitHubCom HMACAuthInfoHost = "https://github.com" ) +// Hook event name dispatched through the SDK callback transport. +// Experimental: HookType is part of an experimental API and may change or be removed. +type HookType string + +const ( + // Runs when the agent stops. + HookTypeAgentStop HookType = "agentStop" + // Runs when the agent encounters an error. + HookTypeErrorOccurred HookType = "errorOccurred" + // Runs when the agent emits a notification. + HookTypeNotification HookType = "notification" + // Runs when the agent requests permission. + HookTypePermissionRequest HookType = "permissionRequest" + // Runs after an agent result is produced. + HookTypePostResult HookType = "postResult" + // Runs after a tool completes successfully. + HookTypePostToolUse HookType = "postToolUse" + // Runs after a tool fails. + HookTypePostToolUseFailure HookType = "postToolUseFailure" + // Runs before conversation context is compacted. + HookTypePreCompact HookType = "preCompact" + // Runs before an MCP tool is invoked. + HookTypePreMCPToolCall HookType = "preMcpToolCall" + // Runs before a pull request description is generated. + HookTypePrePRDescription HookType = "prePRDescription" + // Runs before a tool is invoked. + HookTypePreToolUse HookType = "preToolUse" + // Runs when a session ends. + HookTypeSessionEnd HookType = "sessionEnd" + // Runs when a session starts. + HookTypeSessionStart HookType = "sessionStart" + // Runs when a subagent starts. + HookTypeSubagentStart HookType = "subagentStart" + // Runs when a subagent stops. + HookTypeSubagentStop HookType = "subagentStop" + // Runs after the user submits a prompt. + HookTypeUserPromptSubmitted HookType = "userPromptSubmitted" +) + // Constant value. Always "github". type InstalledPluginSourceGitHubSource string @@ -11411,6 +11789,18 @@ const ( MCPSetEnvValueModeDetailsIndirect MCPSetEnvValueModeDetails = "indirect" ) +// Consumer allowed to call an MCP tool. +// Experimental: MCPToolUIVisibility is part of an experimental API and may change or be +// removed. +type MCPToolUIVisibility string + +const ( + // An MCP App view may call the tool. + MCPToolUIVisibilityApp MCPToolUIVisibility = "app" + // The model may call the tool. + MCPToolUIVisibilityModel MCPToolUIVisibility = "model" +) + // The current agent mode for this session (e.g., 'interactive', 'plan', 'autopilot') // Experimental: MetadataSnapshotCurrentMode is part of an experimental API and may change // or be removed. @@ -12556,6 +12946,19 @@ const ( UserToolSessionApprovalKindWrite UserToolSessionApprovalKind = "write" ) +// Output verbosity level for supported models +// Experimental: Verbosity is part of an experimental API and may change or be removed. +type Verbosity string + +const ( + // Request a more detailed response. + VerbosityHigh Verbosity = "high" + // Request a terse response. + VerbosityLow Verbosity = "low" + // Request a medium amount of response detail. + VerbosityMedium Verbosity = "medium" +) + // Type of change represented by this file diff. // Experimental: WorkspaceDiffFileChangeType is part of an experimental API and may change // or be removed. @@ -12782,6 +13185,29 @@ func (a *ServerAgentsAPI) GetDiscoveryPaths(ctx context.Context, params *AgentsG return &result, nil } +// Experimental: ServerCommandsAPI contains experimental APIs that may change or be removed. +type ServerCommandsAPI serverAPI + +// Lists the well-known built-in slash commands that work as the first message in a new +// session (e.g. /plan, /env), without requiring an active session. Commands that depend on +// session state, authentication, or a synced session are omitted. +// +// RPC method: commands.list. +// +// Returns: Slash commands available in the session, after applying any include/exclude +// filters. +func (a *ServerCommandsAPI) List(ctx context.Context) (*CommandList, error) { + raw, err := a.client.Request(ctx, "commands.list", nil) + if err != nil { + return nil, err + } + var result CommandList + 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 @@ -13199,7 +13625,8 @@ type ServerPluginsMarketplacesAPI serverAPI // // RPC method: plugins.marketplaces.add. // -// Parameters: Marketplace source to register. +// Parameters: Marketplace source and optional working directory for relative-path +// resolution. // // Returns: Result of registering a new marketplace. func (a *ServerPluginsMarketplacesAPI) Add(ctx context.Context, params *PluginsMarketplacesAddRequest) (*MarketplaceAddResult, error) { @@ -14018,6 +14445,7 @@ type ServerRPC struct { Account *ServerAccountAPI AgentRegistry *ServerAgentRegistryAPI Agents *ServerAgentsAPI + Commands *ServerCommandsAPI Instructions *ServerInstructionsAPI LlmInference *ServerLlmInferenceAPI MCP *ServerMCPAPI @@ -14059,6 +14487,7 @@ func NewServerRPC(client *jsonrpc2.Client) *ServerRPC { r.Account = (*ServerAccountAPI)(&r.common) r.AgentRegistry = (*ServerAgentRegistryAPI)(&r.common) r.Agents = (*ServerAgentsAPI)(&r.common) + r.Commands = (*ServerCommandsAPI)(&r.common) r.Instructions = (*ServerInstructionsAPI)(&r.common) r.LlmInference = (*ServerLlmInferenceAPI)(&r.common) r.MCP = (*ServerMCPAPI)(&r.common) @@ -15333,7 +15762,9 @@ func (a *MCPAPI) List(ctx context.Context) (*MCPServerList, error) { return &result, nil } -// ListTools lists the tools exposed by a connected MCP server on this session's host. +// ListTools lists the tools exposed by a connected MCP server on this session's host. This +// performs a live `tools/list` request. Tool UI metadata is returned independently of +// whether MCP Apps rendering is enabled for the session. // // RPC method: session.mcp.listTools. // @@ -15392,6 +15823,35 @@ func (a *MCPAPI) RemoveGitHub(ctx context.Context) (*MCPRemoveGitHubResult, erro return &result, nil } +// RestartServer restarts an individual MCP server on the live session (stops then starts). +// Omit `config` for a config-free restart-by-name of an already-configured server; supply +// `config` to restart with a replacement configuration. Session-scoped and ephemeral: does +// NOT modify persistent user configuration (`mcp.config.*`). +// +// RPC method: session.mcp.restartServer. +// +// Parameters: Server name and optional replacement configuration for an individual MCP +// server restart. Omit `config` for a config-free restart-by-name of an already-configured +// server. +func (a *MCPAPI) RestartServer(ctx context.Context, params *MCPRestartServerRequest) (*SessionMCPRestartServerResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + if params.Config != nil { + req["config"] = params.Config + } + req["serverName"] = params.ServerName + } + raw, err := a.client.Request(ctx, "session.mcp.restartServer", req) + if err != nil { + return nil, err + } + var result SessionMCPRestartServerResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // SetEnvValueMode sets how environment-variable values supplied to MCP servers are resolved // (direct or indirect). // @@ -15417,6 +15877,33 @@ func (a *MCPAPI) SetEnvValueMode(ctx context.Context, params *MCPSetEnvValueMode return &result, nil } +// StartServer starts an individual MCP server on the live session from a caller-supplied +// config. Session-scoped and ephemeral: the server is added to this session's running set +// only and is reaped when the session ends. Does NOT modify persistent user configuration +// (`mcp.config.*`), so it does not affect future sessions. The server surfaces through +// `session.mcp.list` and the `session.mcp_servers_loaded` / +// `session.mcp_server_status_changed` events like any other server. +// +// RPC method: session.mcp.startServer. +// +// Parameters: Server name and configuration for an individual MCP server start. +func (a *MCPAPI) StartServer(ctx context.Context, params *MCPStartServerRequest) (*SessionMCPStartServerResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["config"] = params.Config + req["serverName"] = params.ServerName + } + raw, err := a.client.Request(ctx, "session.mcp.startServer", req) + if err != nil { + return nil, err + } + var result SessionMCPStartServerResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // StopServer stops an individual MCP server on the session's host. // // RPC method: session.mcp.stopServer. @@ -15706,6 +16193,93 @@ func (s *MCPAPI) Oauth() *MCPOauthAPI { return (*MCPOauthAPI)(s) } +// Experimental: MCPResourcesAPI contains experimental APIs that may change or be removed. +type MCPResourcesAPI sessionAPI + +// List enumerate one page of resources a connected MCP server exposes (proxies MCP +// `resources/list`). Pass `cursor` to continue from a prior result's `nextCursor`. +// +// RPC method: session.mcp.resources.list. +// +// Parameters: MCP server whose resources to enumerate. +// +// Returns: One page of resources advertised by the named MCP server. +func (a *MCPResourcesAPI) List(ctx context.Context, params *MCPResourcesListRequest) (*MCPResourcesListResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + if params.Cursor != nil { + req["cursor"] = *params.Cursor + } + req["serverName"] = params.ServerName + } + raw, err := a.client.Request(ctx, "session.mcp.resources.list", req) + if err != nil { + return nil, err + } + var result MCPResourcesListResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ListTemplates enumerate one page of resource templates a connected MCP server exposes +// (proxies MCP `resources/templates/list`). Pass `cursor` to continue from a prior result's +// `nextCursor`. +// +// RPC method: session.mcp.resources.listTemplates. +// +// Parameters: MCP server whose resource templates to enumerate. +// +// Returns: One page of resource templates advertised by the named MCP server. +func (a *MCPResourcesAPI) ListTemplates(ctx context.Context, params *MCPResourcesListTemplatesRequest) (*MCPResourcesListTemplatesResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + if params.Cursor != nil { + req["cursor"] = *params.Cursor + } + req["serverName"] = params.ServerName + } + raw, err := a.client.Request(ctx, "session.mcp.resources.listTemplates", req) + if err != nil { + return nil, err + } + var result MCPResourcesListTemplatesResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Read fetch an MCP resource from a connected server by URI (proxies MCP `resources/read`). +// +// RPC method: session.mcp.resources.read. +// +// Parameters: MCP server and resource URI to fetch. +// +// Returns: Resource contents returned by the MCP server. +func (a *MCPResourcesAPI) Read(ctx context.Context, params *MCPResourcesReadRequest) (*MCPResourcesReadResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["serverName"] = params.ServerName + req["uri"] = params.URI + } + raw, err := a.client.Request(ctx, "session.mcp.resources.read", req) + if err != nil { + return nil, err + } + var result MCPResourcesReadResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: Resources returns experimental APIs that may change or be removed. +func (s *MCPAPI) Resources() *MCPResourcesAPI { + return (*MCPResourcesAPI)(s) +} + // Experimental: MetadataAPI contains experimental APIs that may change or be removed. type MetadataAPI sessionAPI @@ -15882,16 +16456,26 @@ func (a *MetadataAPI) RecordContextChange(ctx context.Context, params *MetadataR return &result, nil } -// SetWorkingDirectory updates the session's recorded working directory. +// SetWorkingDirectory updates the session's working directory. For local sessions the +// target is validated first (an absolute path that exists on disk) and the permission +// primary directory is re-based; a rejected validation fails the call before any session +// state changes. // // RPC method: session.metadata.setWorkingDirectory. // -// Parameters: Absolute path to set as the session's new working directory. +// Parameters: Absolute path to set as the session's new working directory. For local +// sessions the path must be absolute and exist on disk: it is validated before any session +// state changes, and a failing validation rejects the call with nothing mutated, persisted, +// or emitted. Remote sessions record the path as-is. // // Returns: Update the session's working directory. Used by the host when the user -// explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for -// `process.chdir` and any related side-effects (file index, etc.); this method only updates -// the session's own recorded path. +// explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any +// related side-effects (file index, etc.); it does NOT change the process working directory +// (a session's cwd is per-session, not process-global). For local sessions the runtime +// validates the target first (an absolute path that exists on disk) and re-bases the +// permission primary directory; a rejected validation fails the call before anything is +// mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the +// new directory (best-effort). Remote sessions only record the path. func (a *MetadataAPI) SetWorkingDirectory(ctx context.Context, params *MetadataSetWorkingDirectoryRequest) (*MetadataSetWorkingDirectoryResult, error) { req := map[string]any{"sessionId": a.sessionID} if params != nil { @@ -16073,6 +16657,9 @@ func (a *ModelAPI) SwitchTo(ctx context.Context, params *ModelSwitchToRequest) ( if params.ReasoningSummary != nil { req["reasoningSummary"] = *params.ReasoningSummary } + if params.Verbosity != nil { + req["verbosity"] = *params.Verbosity + } } raw, err := a.client.Request(ctx, "session.model.switchTo", req) if err != nil { @@ -16246,6 +16833,9 @@ func (a *OptionsAPI) Update(ctx context.Context, params *SessionUpdateOptionsPar if params.FeatureFlags != nil { req["featureFlags"] = params.FeatureFlags } + if params.IncludedBuiltinAgents != nil { + req["includedBuiltinAgents"] = params.IncludedBuiltinAgents + } if params.InstalledPlugins != nil { req["installedPlugins"] = params.InstalledPlugins } @@ -16321,6 +16911,9 @@ func (a *OptionsAPI) Update(ctx context.Context, params *SessionUpdateOptionsPar if params.TrajectoryFile != nil { req["trajectoryFile"] = *params.TrajectoryFile } + if params.Verbosity != nil { + req["verbosity"] = *params.Verbosity + } if params.WorkingDirectory != nil { req["workingDirectory"] = *params.WorkingDirectory } @@ -17041,6 +17634,9 @@ func (a *PluginsAPI) Reload(ctx context.Context, params ...*PluginsReloadRequest if requestParams.ReloadCustomAgents != nil { req["reloadCustomAgents"] = *requestParams.ReloadCustomAgents } + if requestParams.ReloadExtensions != nil { + req["reloadExtensions"] = *requestParams.ReloadExtensions + } if requestParams.ReloadHooks != nil { req["reloadHooks"] = *requestParams.ReloadHooks } @@ -18610,6 +19206,58 @@ func (a *SessionRPC) Send(ctx context.Context, params *SendRequest) (*SendResult return &result, nil } +// SendMessages sends zero or more user messages to the session in a single turn and returns +// their message IDs. All provided messages are appended to the conversation in order, then +// exactly one agent turn runs over the resulting history. When the list is empty, one turn +// runs over the existing history with no new user message. Remote-backed (Mission Control) +// sessions do not support this method and will return an error. +// +// RPC method: session.sendMessages. +// +// Parameters: Parameters for sending zero or more user messages to the session in a single +// turn. Remote-backed (Mission Control) sessions do not support this method and will return +// an error. +// +// Returns: Result of sending zero or more user messages +// Experimental: SendMessages is an experimental API and may change or be removed in future +// versions. +func (a *SessionRPC) SendMessages(ctx context.Context, params *SendMessagesRequest) (*SendMessagesResult, error) { + req := map[string]any{"sessionId": a.common.sessionID} + if params != nil { + if params.AgentMode != nil { + req["agentMode"] = *params.AgentMode + } + req["messages"] = params.Messages + if params.Mode != nil { + req["mode"] = *params.Mode + } + if params.Prepend != nil { + req["prepend"] = *params.Prepend + } + if params.RequestHeaders != nil { + req["requestHeaders"] = params.RequestHeaders + } + if params.Traceparent != nil { + req["traceparent"] = *params.Traceparent + } + if params.Tracestate != nil { + req["tracestate"] = *params.Tracestate + } + if params.Wait != nil { + req["wait"] = *params.Wait + } + } + raw, err := a.common.client.Request(ctx, "session.sendMessages", req) + if err != nil { + return nil, err + } + var result SendMessagesResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Shutdown shuts down the session and persists its final state. Awaits any deferred // sessionEnd hooks before resolving so user-supplied hook scripts complete before the // runtime tears down. @@ -18789,54 +19437,6 @@ func (a *InternalMCPAPI) ReloadWithConfig(ctx context.Context, params *MCPReload return &result, nil } -// RestartServer restarts an individual MCP server on the session's host (stops then starts). -// -// RPC method: session.mcp.restartServer. -// -// Parameters: Server name and opaque configuration for an individual MCP server restart. -// Internal: RestartServer is part of the SDK's internal handshake/plumbing; external -// callers should not use it. -func (a *InternalMCPAPI) RestartServer(ctx context.Context, params *MCPRestartServerRequest) (*SessionMCPRestartServerResult, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - req["config"] = params.Config - req["serverName"] = params.ServerName - } - raw, err := a.client.Request(ctx, "session.mcp.restartServer", req) - if err != nil { - return nil, err - } - var result SessionMCPRestartServerResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - -// StartServer starts an individual MCP server on the session's host. -// -// RPC method: session.mcp.startServer. -// -// Parameters: Server name and opaque configuration for an individual MCP server start. -// Internal: StartServer is part of the SDK's internal handshake/plumbing; external callers -// should not use it. -func (a *InternalMCPAPI) StartServer(ctx context.Context, params *MCPStartServerRequest) (*SessionMCPStartServerResult, error) { - req := map[string]any{"sessionId": a.sessionID} - if params != nil { - req["config"] = params.Config - req["serverName"] = params.ServerName - } - raw, err := a.client.Request(ctx, "session.mcp.startServer", req) - if err != nil { - return nil, err - } - var result SessionMCPStartServerResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - // UnregisterExternalClient unregisters a previously registered external MCP client by // server name. Marked internal as the paired companion of `registerExternalClient`: only // in-process callers that registered a client this way can meaningfully unregister it. @@ -19452,6 +20052,20 @@ type GitHubTelemetryHandler interface { Event(request *GitHubTelemetryNotification) error } +// Experimental: HooksHandler contains experimental APIs that may change or be removed. +type HooksHandler interface { + // Invoke dispatches one SDK callback hook from the runtime to the connection that + // registered it. Internal transport plumbing: clients opt in through session initialization + // and the Rust hook processor owns ordering, policy, timeout, and callback routing. + // + // RPC method: hooks.invoke. + // + // Parameters: Runtime-owned wire payload for a server-to-client hook callback invocation. + // + // Returns: Optional output returned by an SDK callback hook. + Invoke(request *HookInvokeRequest) (*HookInvokeResponse, error) +} + // Experimental: LlmInferenceHandler contains experimental APIs that may change or be // removed. type LlmInferenceHandler interface { @@ -19489,6 +20103,7 @@ type LlmInferenceHandler interface { // key; a single set of handlers serves the entire connection. type ClientGlobalAPIHandlers struct { GitHubTelemetry GitHubTelemetryHandler + Hooks HooksHandler LlmInference LlmInferenceHandler } @@ -19519,6 +20134,24 @@ func RegisterClientGlobalAPIHandlers(client *jsonrpc2.Client, handlers *ClientGl } return nil, nil }) + client.SetRequestHandler("hooks.invoke", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request HookInvokeRequest + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} + } + if handlers == nil || handlers.Hooks == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: "No hooks client-global handler registered"} + } + result, err := handlers.Hooks.Invoke(&request) + if err != nil { + return nil, clientGlobalHandlerError(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 + }) client.SetRequestHandler("llmInference.httpRequestChunk", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { var request LlmInferenceHTTPRequestChunkRequest if err := json.Unmarshal(params, &request); err != nil { diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index b87db8a8b7..81e693120b 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -1561,6 +1561,46 @@ func (r *MCPOauthHandlePendingRequest) UnmarshalJSON(data []byte) error { return nil } +func (r *MCPRestartServerRequest) UnmarshalJSON(data []byte) error { + type rawMCPRestartServerRequest struct { + Config json.RawMessage `json:"config,omitempty"` + ServerName string `json:"serverName"` + } + var raw rawMCPRestartServerRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Config != nil { + value, err := unmarshalMCPServerConfig(raw.Config) + if err != nil { + return err + } + r.Config = value + } + r.ServerName = raw.ServerName + return nil +} + +func (r *MCPStartServerRequest) UnmarshalJSON(data []byte) error { + type rawMCPStartServerRequest struct { + Config json.RawMessage `json:"config"` + ServerName string `json:"serverName"` + } + var raw rawMCPStartServerRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Config != nil { + value, err := unmarshalMCPServerConfig(raw.Config) + if err != nil { + return err + } + r.Config = value + } + r.ServerName = raw.ServerName + return nil +} + func unmarshalPermissionDecision(data []byte) (PermissionDecision, error) { if string(data) == "null" { return nil, nil @@ -3136,6 +3176,37 @@ func (r *SendAttachmentsToMessageParams) UnmarshalJSON(data []byte) error { return nil } +func (r *SendMessageItem) UnmarshalJSON(data []byte) error { + type rawSendMessageItem struct { + Attachments []json.RawMessage `json:"attachments,omitzero"` + Billable *bool `json:"billable,omitempty"` + DisplayPrompt *string `json:"displayPrompt,omitempty"` + Prompt string `json:"prompt"` + RequiredTool *string `json:"requiredTool,omitempty"` + Source *string `json:"source,omitempty"` + } + var raw rawSendMessageItem + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Attachments != nil { + r.Attachments = make([]Attachment, 0, len(raw.Attachments)) + for _, rawItem := range raw.Attachments { + value, err := unmarshalAttachment(rawItem) + if err != nil { + return err + } + r.Attachments = append(r.Attachments, value) + } + } + r.Billable = raw.Billable + r.DisplayPrompt = raw.DisplayPrompt + r.Prompt = raw.Prompt + r.RequiredTool = raw.RequiredTool + r.Source = raw.Source + return nil +} + func (r *SendRequest) UnmarshalJSON(data []byte) error { type rawSendRequest struct { AgentMode *SendAgentMode `json:"agentMode,omitempty"` @@ -3329,6 +3400,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { DisabledInstructionSources []string `json:"disabledInstructionSources,omitzero"` DisabledSkills []string `json:"disabledSkills,omitzero"` EnableCitations *bool `json:"enableCitations,omitempty"` + EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` EnableOnDemandInstructionDiscovery *bool `json:"enableOnDemandInstructionDiscovery,omitempty"` EnableScriptSafety *bool `json:"enableScriptSafety,omitempty"` EnableStreaming *bool `json:"enableStreaming,omitempty"` @@ -3338,6 +3410,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"` + IncludedBuiltinAgents []string `json:"includedBuiltinAgents,omitzero"` InstalledPlugins []InstalledPlugin `json:"installedPlugins,omitzero"` IntegrationID *string `json:"integrationId,omitempty"` IsExperimentalMode *bool `json:"isExperimentalMode,omitempty"` @@ -3358,7 +3431,6 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { RemoteSteerable *bool `json:"remoteSteerable,omitempty"` RunningInInteractiveMode *bool `json:"runningInInteractiveMode,omitempty"` SandboxConfig *SandboxConfig `json:"sandboxConfig,omitempty"` - SelfFetchManagedSettings *bool `json:"selfFetchManagedSettings,omitempty"` SessionCapabilities []SessionCapability `json:"sessionCapabilities,omitzero"` SessionID *string `json:"sessionId,omitempty"` SessionLimits *SessionLimitsConfig `json:"sessionLimits,omitempty"` @@ -3367,6 +3439,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { SkillDirectories []string `json:"skillDirectories,omitzero"` SkipCustomInstructions *bool `json:"skipCustomInstructions,omitempty"` TrajectoryFile *string `json:"trajectoryFile,omitempty"` + Verbosity *Verbosity `json:"verbosity,omitempty"` WorkingDirectory *string `json:"workingDirectory,omitempty"` WorkingDirectoryContext *SessionContext `json:"workingDirectoryContext,omitempty"` } @@ -3399,6 +3472,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.DisabledInstructionSources = raw.DisabledInstructionSources r.DisabledSkills = raw.DisabledSkills r.EnableCitations = raw.EnableCitations + r.EnableManagedSettings = raw.EnableManagedSettings r.EnableOnDemandInstructionDiscovery = raw.EnableOnDemandInstructionDiscovery r.EnableScriptSafety = raw.EnableScriptSafety r.EnableStreaming = raw.EnableStreaming @@ -3408,6 +3482,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.ExcludedTools = raw.ExcludedTools r.ExpAssignments = raw.ExpAssignments r.FeatureFlags = raw.FeatureFlags + r.IncludedBuiltinAgents = raw.IncludedBuiltinAgents r.InstalledPlugins = raw.InstalledPlugins r.IntegrationID = raw.IntegrationID r.IsExperimentalMode = raw.IsExperimentalMode @@ -3428,7 +3503,6 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.RemoteSteerable = raw.RemoteSteerable r.RunningInInteractiveMode = raw.RunningInInteractiveMode r.SandboxConfig = raw.SandboxConfig - r.SelfFetchManagedSettings = raw.SelfFetchManagedSettings r.SessionCapabilities = raw.SessionCapabilities r.SessionID = raw.SessionID r.SessionLimits = raw.SessionLimits @@ -3437,6 +3511,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.SkillDirectories = raw.SkillDirectories r.SkipCustomInstructions = raw.SkipCustomInstructions r.TrajectoryFile = raw.TrajectoryFile + r.Verbosity = raw.Verbosity r.WorkingDirectory = raw.WorkingDirectory r.WorkingDirectoryContext = raw.WorkingDirectoryContext return nil diff --git a/go/rpc/zsession_encoding.go b/go/rpc/zsession_encoding.go index 85c1bd4497..2bd212d884 100644 --- a/go/rpc/zsession_encoding.go +++ b/go/rpc/zsession_encoding.go @@ -83,12 +83,24 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeAssistantServerToolProgress: + var d AssistantServerToolProgressData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeAssistantStreamingDelta: var d AssistantStreamingDeltaData if err := json.Unmarshal(raw.Data, &d); err != nil { return err } e.Data = &d + case SessionEventTypeAssistantToolCallDelta: + var d AssistantToolCallDeltaData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeAssistantTurnEnd: var d AssistantTurnEndData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -233,6 +245,24 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeMCPPromptsListChanged: + var d MCPPromptsListChangedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d + case SessionEventTypeMCPResourcesListChanged: + var d MCPResourcesListChangedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d + case SessionEventTypeMCPToolsListChanged: + var d MCPToolsListChangedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeModelCallFailure: var d ModelCallFailureData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -269,6 +299,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeSessionAutoModeResolved: + var d SessionAutoModeResolvedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeSessionAutopilotObjectiveChanged: var d SessionAutopilotObjectiveChangedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -401,6 +437,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeSessionManagedSettingsResolved: + var d SessionManagedSettingsResolvedData + 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 { @@ -1218,6 +1260,7 @@ func (r *ToolExecutionCompleteResult) UnmarshalJSON(data []byte) error { Content string `json:"content"` Contents []json.RawMessage `json:"contents,omitzero"` DetailedContent *string `json:"detailedContent,omitempty"` + MCPMeta any `json:"mcpMeta,omitempty"` StructuredContent any `json:"structuredContent,omitempty"` UIResource *ToolExecutionCompleteUIResource `json:"uiResource,omitempty"` } @@ -1248,6 +1291,7 @@ func (r *ToolExecutionCompleteResult) UnmarshalJSON(data []byte) error { } } r.DetailedContent = raw.DetailedContent + r.MCPMeta = raw.MCPMeta r.StructuredContent = raw.StructuredContent r.UIResource = raw.UIResource return nil diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index b16d76a9a7..ec211132df 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -53,45 +53,53 @@ func (r RawSessionEventData) Type() SessionEventType { type SessionEventType string const ( - SessionEventTypeAbort SessionEventType = "abort" - SessionEventTypeAssistantIdle SessionEventType = "assistant.idle" - SessionEventTypeAssistantIntent SessionEventType = "assistant.intent" - SessionEventTypeAssistantMessage SessionEventType = "assistant.message" - SessionEventTypeAssistantMessageDelta SessionEventType = "assistant.message_delta" - SessionEventTypeAssistantMessageStart SessionEventType = "assistant.message_start" - SessionEventTypeAssistantReasoning SessionEventType = "assistant.reasoning" - SessionEventTypeAssistantReasoningDelta SessionEventType = "assistant.reasoning_delta" - SessionEventTypeAssistantStreamingDelta SessionEventType = "assistant.streaming_delta" - SessionEventTypeAssistantTurnEnd SessionEventType = "assistant.turn_end" - SessionEventTypeAssistantTurnStart SessionEventType = "assistant.turn_start" - SessionEventTypeAssistantUsage SessionEventType = "assistant.usage" - SessionEventTypeAutoModeSwitchCompleted SessionEventType = "auto_mode_switch.completed" - SessionEventTypeAutoModeSwitchRequested SessionEventType = "auto_mode_switch.requested" - SessionEventTypeCapabilitiesChanged SessionEventType = "capabilities.changed" - SessionEventTypeCommandCompleted SessionEventType = "command.completed" - SessionEventTypeCommandExecute SessionEventType = "command.execute" - SessionEventTypeCommandQueued SessionEventType = "command.queued" - SessionEventTypeCommandsChanged SessionEventType = "commands.changed" - SessionEventTypeElicitationCompleted SessionEventType = "elicitation.completed" - SessionEventTypeElicitationRequested SessionEventType = "elicitation.requested" - SessionEventTypeExitPlanModeCompleted SessionEventType = "exit_plan_mode.completed" - SessionEventTypeExitPlanModeRequested SessionEventType = "exit_plan_mode.requested" - SessionEventTypeExternalToolCompleted SessionEventType = "external_tool.completed" - SessionEventTypeExternalToolRequested SessionEventType = "external_tool.requested" - SessionEventTypeHookEnd SessionEventType = "hook.end" - SessionEventTypeHookProgress SessionEventType = "hook.progress" - SessionEventTypeHookStart SessionEventType = "hook.start" - SessionEventTypeMCPAppToolCallComplete SessionEventType = "mcp_app.tool_call_complete" - SessionEventTypeMCPHeadersRefreshCompleted SessionEventType = "mcp.headers_refresh_completed" - SessionEventTypeMCPHeadersRefreshRequired SessionEventType = "mcp.headers_refresh_required" - SessionEventTypeMCPOauthCompleted SessionEventType = "mcp.oauth_completed" - SessionEventTypeMCPOauthRequired SessionEventType = "mcp.oauth_required" - SessionEventTypeModelCallFailure SessionEventType = "model.call_failure" - SessionEventTypePendingMessagesModified SessionEventType = "pending_messages.modified" - SessionEventTypePermissionCompleted SessionEventType = "permission.completed" - SessionEventTypePermissionRequested SessionEventType = "permission.requested" - SessionEventTypeSamplingCompleted SessionEventType = "sampling.completed" - SessionEventTypeSamplingRequested SessionEventType = "sampling.requested" + SessionEventTypeAbort SessionEventType = "abort" + SessionEventTypeAssistantIdle SessionEventType = "assistant.idle" + SessionEventTypeAssistantIntent SessionEventType = "assistant.intent" + SessionEventTypeAssistantMessage SessionEventType = "assistant.message" + SessionEventTypeAssistantMessageDelta SessionEventType = "assistant.message_delta" + SessionEventTypeAssistantMessageStart SessionEventType = "assistant.message_start" + SessionEventTypeAssistantReasoning SessionEventType = "assistant.reasoning" + SessionEventTypeAssistantReasoningDelta SessionEventType = "assistant.reasoning_delta" + SessionEventTypeAssistantServerToolProgress SessionEventType = "assistant.server_tool_progress" + SessionEventTypeAssistantStreamingDelta SessionEventType = "assistant.streaming_delta" + SessionEventTypeAssistantToolCallDelta SessionEventType = "assistant.tool_call_delta" + SessionEventTypeAssistantTurnEnd SessionEventType = "assistant.turn_end" + SessionEventTypeAssistantTurnStart SessionEventType = "assistant.turn_start" + SessionEventTypeAssistantUsage SessionEventType = "assistant.usage" + SessionEventTypeAutoModeSwitchCompleted SessionEventType = "auto_mode_switch.completed" + SessionEventTypeAutoModeSwitchRequested SessionEventType = "auto_mode_switch.requested" + SessionEventTypeCapabilitiesChanged SessionEventType = "capabilities.changed" + SessionEventTypeCommandCompleted SessionEventType = "command.completed" + SessionEventTypeCommandExecute SessionEventType = "command.execute" + SessionEventTypeCommandQueued SessionEventType = "command.queued" + SessionEventTypeCommandsChanged SessionEventType = "commands.changed" + SessionEventTypeElicitationCompleted SessionEventType = "elicitation.completed" + SessionEventTypeElicitationRequested SessionEventType = "elicitation.requested" + SessionEventTypeExitPlanModeCompleted SessionEventType = "exit_plan_mode.completed" + SessionEventTypeExitPlanModeRequested SessionEventType = "exit_plan_mode.requested" + SessionEventTypeExternalToolCompleted SessionEventType = "external_tool.completed" + SessionEventTypeExternalToolRequested SessionEventType = "external_tool.requested" + SessionEventTypeHookEnd SessionEventType = "hook.end" + SessionEventTypeHookProgress SessionEventType = "hook.progress" + SessionEventTypeHookStart SessionEventType = "hook.start" + SessionEventTypeMCPAppToolCallComplete SessionEventType = "mcp_app.tool_call_complete" + SessionEventTypeMCPHeadersRefreshCompleted SessionEventType = "mcp.headers_refresh_completed" + SessionEventTypeMCPHeadersRefreshRequired SessionEventType = "mcp.headers_refresh_required" + SessionEventTypeMCPOauthCompleted SessionEventType = "mcp.oauth_completed" + SessionEventTypeMCPOauthRequired SessionEventType = "mcp.oauth_required" + SessionEventTypeMCPPromptsListChanged SessionEventType = "mcp.prompts.list_changed" + SessionEventTypeMCPResourcesListChanged SessionEventType = "mcp.resources.list_changed" + SessionEventTypeMCPToolsListChanged SessionEventType = "mcp.tools.list_changed" + SessionEventTypeModelCallFailure SessionEventType = "model.call_failure" + SessionEventTypePendingMessagesModified SessionEventType = "pending_messages.modified" + SessionEventTypePermissionCompleted SessionEventType = "permission.completed" + SessionEventTypePermissionRequested SessionEventType = "permission.requested" + SessionEventTypeSamplingCompleted SessionEventType = "sampling.completed" + SessionEventTypeSamplingRequested SessionEventType = "sampling.requested" + // Experimental: SessionEventTypeSessionAutoModeResolved identifies an experimental event + // that may change or be removed. + SessionEventTypeSessionAutoModeResolved SessionEventType = "session.auto_mode_resolved" SessionEventTypeSessionAutopilotObjectiveChanged SessionEventType = "session.autopilot_objective_changed" SessionEventTypeSessionBackgroundTasksChanged SessionEventType = "session.background_tasks_changed" // Experimental: SessionEventTypeSessionBinaryAsset identifies an experimental event that @@ -128,47 +136,50 @@ const ( SessionEventTypeSessionInfo SessionEventType = "session.info" SessionEventTypeSessionLimitsExhaustedCompleted SessionEventType = "session_limits_exhausted.completed" SessionEventTypeSessionLimitsExhaustedRequested SessionEventType = "session_limits_exhausted.requested" - SessionEventTypeSessionMCPServersLoaded SessionEventType = "session.mcp_servers_loaded" - SessionEventTypeSessionMCPServerStatusChanged SessionEventType = "session.mcp_server_status_changed" - SessionEventTypeSessionModeChanged SessionEventType = "session.mode_changed" - SessionEventTypeSessionModelChange SessionEventType = "session.model_change" - SessionEventTypeSessionPermissionsChanged SessionEventType = "session.permissions_changed" - SessionEventTypeSessionPlanChanged SessionEventType = "session.plan_changed" - SessionEventTypeSessionRemoteSteerableChanged SessionEventType = "session.remote_steerable_changed" - SessionEventTypeSessionResume SessionEventType = "session.resume" - SessionEventTypeSessionScheduleCancelled SessionEventType = "session.schedule_cancelled" - SessionEventTypeSessionScheduleCreated SessionEventType = "session.schedule_created" - SessionEventTypeSessionScheduleRearmed SessionEventType = "session.schedule_rearmed" - SessionEventTypeSessionSessionLimitsChanged SessionEventType = "session.session_limits_changed" - SessionEventTypeSessionShutdown SessionEventType = "session.shutdown" - SessionEventTypeSessionSkillsLoaded SessionEventType = "session.skills_loaded" - SessionEventTypeSessionSnapshotRewind SessionEventType = "session.snapshot_rewind" - SessionEventTypeSessionStart SessionEventType = "session.start" - SessionEventTypeSessionTaskComplete SessionEventType = "session.task_complete" - SessionEventTypeSessionTitleChanged SessionEventType = "session.title_changed" - SessionEventTypeSessionTodosChanged SessionEventType = "session.todos_changed" - SessionEventTypeSessionToolsUpdated SessionEventType = "session.tools_updated" - SessionEventTypeSessionTruncation SessionEventType = "session.truncation" - SessionEventTypeSessionUsageCheckpoint SessionEventType = "session.usage_checkpoint" - SessionEventTypeSessionUsageInfo SessionEventType = "session.usage_info" - SessionEventTypeSessionWarning SessionEventType = "session.warning" - SessionEventTypeSessionWorkspaceFileChanged SessionEventType = "session.workspace_file_changed" - SessionEventTypeSkillInvoked SessionEventType = "skill.invoked" - SessionEventTypeSubagentCompleted SessionEventType = "subagent.completed" - SessionEventTypeSubagentDeselected SessionEventType = "subagent.deselected" - SessionEventTypeSubagentFailed SessionEventType = "subagent.failed" - SessionEventTypeSubagentSelected SessionEventType = "subagent.selected" - SessionEventTypeSubagentStarted SessionEventType = "subagent.started" - SessionEventTypeSystemMessage SessionEventType = "system.message" - SessionEventTypeSystemNotification SessionEventType = "system.notification" - SessionEventTypeToolExecutionComplete SessionEventType = "tool.execution_complete" - SessionEventTypeToolExecutionPartialResult SessionEventType = "tool.execution_partial_result" - SessionEventTypeToolExecutionProgress SessionEventType = "tool.execution_progress" - SessionEventTypeToolExecutionStart SessionEventType = "tool.execution_start" - SessionEventTypeToolUserRequested SessionEventType = "tool.user_requested" - SessionEventTypeUserInputCompleted SessionEventType = "user_input.completed" - SessionEventTypeUserInputRequested SessionEventType = "user_input.requested" - SessionEventTypeUserMessage SessionEventType = "user.message" + // Experimental: SessionEventTypeSessionManagedSettingsResolved identifies an experimental + // event that may change or be removed. + SessionEventTypeSessionManagedSettingsResolved SessionEventType = "session.managed_settings_resolved" + SessionEventTypeSessionMCPServersLoaded SessionEventType = "session.mcp_servers_loaded" + SessionEventTypeSessionMCPServerStatusChanged SessionEventType = "session.mcp_server_status_changed" + SessionEventTypeSessionModeChanged SessionEventType = "session.mode_changed" + SessionEventTypeSessionModelChange SessionEventType = "session.model_change" + SessionEventTypeSessionPermissionsChanged SessionEventType = "session.permissions_changed" + SessionEventTypeSessionPlanChanged SessionEventType = "session.plan_changed" + SessionEventTypeSessionRemoteSteerableChanged SessionEventType = "session.remote_steerable_changed" + SessionEventTypeSessionResume SessionEventType = "session.resume" + SessionEventTypeSessionScheduleCancelled SessionEventType = "session.schedule_cancelled" + SessionEventTypeSessionScheduleCreated SessionEventType = "session.schedule_created" + SessionEventTypeSessionScheduleRearmed SessionEventType = "session.schedule_rearmed" + SessionEventTypeSessionSessionLimitsChanged SessionEventType = "session.session_limits_changed" + SessionEventTypeSessionShutdown SessionEventType = "session.shutdown" + SessionEventTypeSessionSkillsLoaded SessionEventType = "session.skills_loaded" + SessionEventTypeSessionSnapshotRewind SessionEventType = "session.snapshot_rewind" + SessionEventTypeSessionStart SessionEventType = "session.start" + SessionEventTypeSessionTaskComplete SessionEventType = "session.task_complete" + SessionEventTypeSessionTitleChanged SessionEventType = "session.title_changed" + SessionEventTypeSessionTodosChanged SessionEventType = "session.todos_changed" + SessionEventTypeSessionToolsUpdated SessionEventType = "session.tools_updated" + SessionEventTypeSessionTruncation SessionEventType = "session.truncation" + SessionEventTypeSessionUsageCheckpoint SessionEventType = "session.usage_checkpoint" + SessionEventTypeSessionUsageInfo SessionEventType = "session.usage_info" + SessionEventTypeSessionWarning SessionEventType = "session.warning" + SessionEventTypeSessionWorkspaceFileChanged SessionEventType = "session.workspace_file_changed" + SessionEventTypeSkillInvoked SessionEventType = "skill.invoked" + SessionEventTypeSubagentCompleted SessionEventType = "subagent.completed" + SessionEventTypeSubagentDeselected SessionEventType = "subagent.deselected" + SessionEventTypeSubagentFailed SessionEventType = "subagent.failed" + SessionEventTypeSubagentSelected SessionEventType = "subagent.selected" + SessionEventTypeSubagentStarted SessionEventType = "subagent.started" + SessionEventTypeSystemMessage SessionEventType = "system.message" + SessionEventTypeSystemNotification SessionEventType = "system.notification" + SessionEventTypeToolExecutionComplete SessionEventType = "tool.execution_complete" + SessionEventTypeToolExecutionPartialResult SessionEventType = "tool.execution_partial_result" + SessionEventTypeToolExecutionProgress SessionEventType = "tool.execution_progress" + SessionEventTypeToolExecutionStart SessionEventType = "tool.execution_start" + SessionEventTypeToolUserRequested SessionEventType = "tool.user_requested" + SessionEventTypeUserInputCompleted SessionEventType = "user_input.completed" + SessionEventTypeUserInputRequested SessionEventType = "user_input.requested" + SessionEventTypeUserMessage SessionEventType = "user.message" ) // Agent intent description for current activity or plan @@ -209,6 +220,8 @@ type AssistantMessageData struct { // Provider-agnostic citations linking spans of this message's content to the sources that support them. Experimental; only populated when citation emission is enabled. // Experimental: Citations is part of an experimental API and may change or be removed. Citations *Citations `json:"citations,omitempty"` + // Client-minted request id (x-request-id header) echoed by the server. Distinct from requestId (x-github-request-id) and serviceRequestId (x-copilot-service-request-id). + ClientRequestID *string `json:"clientRequestId,omitempty"` // The assistant's text response content Content string `json:"content"` // Encrypted reasoning content from OpenAI models. Session-bound and stripped on resume. @@ -247,6 +260,28 @@ type AssistantMessageData struct { func (*AssistantMessageData) sessionEventData() {} func (*AssistantMessageData) Type() SessionEventType { return SessionEventTypeAssistantMessage } +// Auto Intent resolution: the concrete model the session settled on for the first prompt of an auto-mode session, and why. Lets SDK clients render the chosen model and the full reason it was picked. The core selection fields (chosenModel/reasoningBucket/categoryScores) are stable; the routing-analytics fields (predictedLabel/confidence/candidateModels) mirror the upstream intent service and may evolve, hence the event's experimental stability. +// Experimental: SessionAutoModeResolvedData is part of an experimental API and may change or be removed. +type SessionAutoModeResolvedData struct { + // Ordered candidate model list the router returned, when not a fallback + CandidateModels []string `json:"candidateModels,omitzero"` + // Per-category classifier scores (0-1) behind the bucket: the granular HYDRA capability scores (reasoning, code_gen, debugging, tool_use), or the binary needs_reasoning/no_reasoning scores when HYDRA didn't run. Lets clients show a breakdown rather than just the bucket. + CategoryScores map[string]float64 `json:"categoryScores,omitzero"` + // The concrete model the session will use after any intent refinement + ChosenModel string `json:"chosenModel"` + // Classifier confidence for the predicted label, when available + Confidence *float64 `json:"confidence,omitempty"` + // The predicted classifier label (e.g. `needs_reasoning`), when available + PredictedLabel *string `json:"predictedLabel,omitempty"` + // Coarse request-difficulty bucket, for explaining why a model was chosen ("picked X because this looks like high-reasoning work") + ReasoningBucket *AutoModeResolvedReasoningBucket `json:"reasoningBucket,omitempty"` +} + +func (*SessionAutoModeResolvedData) sessionEventData() {} +func (*SessionAutoModeResolvedData) Type() SessionEventType { + return SessionEventTypeSessionAutoModeResolved +} + // Auto mode switch completion notification type AutoModeSwitchCompletedData struct { // Request ID of the resolved request; clients should dismiss any UI for this request @@ -554,6 +589,30 @@ func (*PendingMessagesModifiedData) Type() SessionEventType { return SessionEventTypePendingMessagesModified } +// Enterprise managed-settings resolution: the effective managed settings the session applied and where they came from, so SDK clients can show users what is enterprise-managed and by which authority. 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; for a session-independent pull, use the SDK `getManagedSettings()` API, which returns the identical payload. Managed settings have a single authoritative source, so the highest-authority present layer (server > device) wins wholesale; `bypassPermissionsDisabled` is deny-wins across layers. 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. + BypassPermissionsDisabled bool `json:"bypassPermissionsDisabled"` + // Whether the device (MDM/plist/registry/file) managed-settings layer was present + DeviceManaged bool `json:"deviceManaged"` + // 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. + FailClosed bool `json:"failClosed"` + // The setting keys under enterprise management in the effective managed settings (e.g. `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force. + ManagedKeys []string `json:"managedKeys"` + // 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"` + // Which channel supplied the effective managed settings (the winning layer), or `none` when no policy is in force + Source ManagedSettingsResolvedSource `json:"source"` +} + +func (*SessionManagedSettingsResolvedData) sessionEventData() {} +func (*SessionManagedSettingsResolvedData) Type() SessionEventType { + return SessionEventTypeSessionManagedSettingsResolved +} + // Ephemeral progress update from a running hook process type HookProgressData struct { // Human-readable progress message from the hook process @@ -759,6 +818,21 @@ type AssistantUsageData struct { func (*AssistantUsageData) sessionEventData() {} func (*AssistantUsageData) Type() SessionEventType { return SessionEventTypeAssistantUsage } +// Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message +type AssistantServerToolProgressData struct { + // Kind of hosted server tool that is running. Only `web_search` is emitted today. + Kind string `json:"kind"` + // Position of the hosted tool call in the response output. Stable across the call's lifecycle events (unlike the provider's per-event item id, which CAPI rotates), so the host keys the live in-progress row on it. + OutputIndex int64 `json:"outputIndex"` + // Lifecycle status of the hosted call: `in_progress`, `searching`, or `completed`. + Status string `json:"status"` +} + +func (*AssistantServerToolProgressData) sessionEventData() {} +func (*AssistantServerToolProgressData) Type() SessionEventType { + return SessionEventTypeAssistantServerToolProgress +} + // MCP App view called a tool on a connected MCP server (SEP-1865) type MCPAppToolCallCompleteData struct { // Arguments passed to the tool by the app view, if any @@ -822,10 +896,14 @@ type SessionModelChangeData struct { PreviousReasoningEffort *string `json:"previousReasoningEffort,omitempty"` // Reasoning summary mode before the model change, if applicable PreviousReasoningSummary *ReasoningSummary `json:"previousReasoningSummary,omitempty"` + // Output verbosity level before the model change, if applicable + PreviousVerbosity *Verbosity `json:"previousVerbosity,omitempty"` // Reasoning effort level after the model change, if applicable ReasoningEffort *string `json:"reasoningEffort,omitempty"` // Reasoning summary mode after the model change, if applicable ReasoningSummary *ReasoningSummary `json:"reasoningSummary,omitempty"` + // Output verbosity level after the model change, if applicable + Verbosity *Verbosity `json:"verbosity,omitempty"` } func (*SessionModelChangeData) sessionEventData() {} @@ -844,6 +922,8 @@ func (*SessionRemoteSteerableChangedData) Type() SessionEventType { // OAuth authentication request for an MCP server type MCPOauthRequiredData struct { + // Raw HTTP response details from the OAuth auth challenge, as observed by the runtime. Header order and casing are transport-dependent, and duplicate header names may appear multiple times. + HTTPResponse *MCPOauthHTTPResponse `json:"httpResponse,omitempty"` // Why the runtime is requesting host-provided OAuth credentials. Reason MCPOauthRequestReason `json:"reason"` // Unique identifier for this OAuth request; used to respond via session.mcp.oauth.handlePendingRequest @@ -891,6 +971,37 @@ type AssistantIdleData struct { func (*AssistantIdleData) sessionEventData() {} func (*AssistantIdleData) Type() SessionEventType { return SessionEventTypeAssistantIdle } +// Payload identifying the MCP server associated with a list change. +type MCPPromptsListChangedData struct { + // Name of the MCP server whose list changed + ServerName string `json:"serverName"` +} + +func (*MCPPromptsListChangedData) sessionEventData() {} +func (*MCPPromptsListChangedData) Type() SessionEventType { + return SessionEventTypeMCPPromptsListChanged +} + +// Payload identifying the MCP server associated with a list change. +type MCPResourcesListChangedData struct { + // Name of the MCP server whose list changed + ServerName string `json:"serverName"` +} + +func (*MCPResourcesListChangedData) sessionEventData() {} +func (*MCPResourcesListChangedData) Type() SessionEventType { + return SessionEventTypeMCPResourcesListChanged +} + +// Payload identifying the MCP server associated with a list change. +type MCPToolsListChangedData struct { + // Name of the MCP server whose list changed + ServerName string `json:"serverName"` +} + +func (*MCPToolsListChangedData) sessionEventData() {} +func (*MCPToolsListChangedData) Type() SessionEventType { return SessionEventTypeMCPToolsListChanged } + // Payload indicating the session is idle with no background agents or attached shell commands in flight type SessionIdleData struct { // True when the preceding agentic loop was cancelled via abort signal @@ -914,7 +1025,7 @@ type SessionCanvasClosedData struct { func (*SessionCanvasClosedData) sessionEventData() {} func (*SessionCanvasClosedData) Type() SessionEventType { return SessionEventTypeSessionCanvasClosed } -// Payload of `session.canvas.opened` with canvas instance and provider IDs plus optional title, status, URL, and input. +// Payload of `session.canvas.opened` with canvas instance and provider IDs plus optional icon, title, status, URL, and input. // Experimental: SessionCanvasOpenedData is part of an experimental API and may change or be removed. type SessionCanvasOpenedData struct { // Provider-local canvas identifier @@ -923,6 +1034,8 @@ type SessionCanvasOpenedData struct { ExtensionID string `json:"extensionId"` // Owning extension display name, when available ExtensionName *string `json:"extensionName,omitempty"` + // Host-local PNG path for the canvas icon, when supplied + Icon *string `json:"icon,omitempty"` // Input supplied when the instance was opened Input any `json:"input,omitempty"` // Stable caller-supplied canvas instance identifier @@ -1329,6 +1442,8 @@ type SessionStartData struct { SessionLimits *SessionLimitsConfig `json:"sessionLimits,omitempty"` // ISO 8601 timestamp when the session was created StartTime time.Time `json:"startTime"` + // Output verbosity level used for model calls, if applicable (e.g. "low", "medium", "high") + Verbosity *Verbosity `json:"verbosity,omitempty"` // Schema version number for the session event format Version int64 `json:"version"` } @@ -1403,6 +1518,8 @@ type SessionResumeData struct { SessionLimits *SessionLimitsConfig `json:"sessionLimits,omitempty"` // True when this resume attached to a session that the runtime already had running in-memory (for example, an extension joining a session another client was actively driving). False (or omitted) for cold resumes — the runtime had to reconstitute the session from its persisted event log. SessionWasActive *bool `json:"sessionWasActive,omitempty"` + // Output verbosity level used for model calls, if applicable (e.g. "low", "medium", "high") + Verbosity *Verbosity `json:"verbosity,omitempty"` } func (*SessionResumeData) sessionEventData() {} @@ -1569,6 +1686,23 @@ func (*ToolExecutionPartialResultData) Type() SessionEventType { return SessionEventTypeToolExecutionPartialResult } +// Streaming tool-call input delta for incremental tool-call updates +type AssistantToolCallDeltaData struct { + // Raw provider tool input fragment to append for this tool call. Function/tool-use providers stream serialized JSON argument text (so newlines inside JSON string values may appear as escaped `\n` until the accumulated JSON is parsed); custom tool calls stream raw custom input. + InputDelta string `json:"inputDelta"` + // Tool call ID this delta belongs to, matching the corresponding assistant.message tool request + ToolCallID string `json:"toolCallId"` + // Name of the tool being invoked, when known from the stream + ToolName *string `json:"toolName,omitempty"` + // Tool call type, when known from the stream + ToolType *AssistantMessageToolRequestType `json:"toolType,omitempty"` +} + +func (*AssistantToolCallDeltaData) sessionEventData() {} +func (*AssistantToolCallDeltaData) Type() SessionEventType { + return SessionEventTypeAssistantToolCallDelta +} + // Sub-agent completion details for successful execution type SubagentCompletedData struct { // Human-readable display name of the sub-agent @@ -1675,6 +1809,9 @@ type ToolExecutionCompleteData struct { InteractionID *string `json:"interactionId,omitempty"` // Whether this tool call was explicitly requested by the user rather than the assistant IsUserRequested *bool `json:"isUserRequested,omitempty"` + // FIDES IFC label projected from tool ingress metadata (MCP `CallToolResult._meta` or synthesized built-in ingress labels). Persisted as `{ ifc: ... }` so the label survives session resume, including model-visible failure results. Experimental. + // Experimental: MCPMeta is part of an experimental API and may change or be removed. + MCPMeta any `json:"mcpMeta,omitempty"` // Model identifier that generated this tool call Model *string `json:"model,omitempty"` // Tool call ID of the parent tool invocation when this event originates from a sub-agent @@ -1991,6 +2128,8 @@ type CanvasRegistryChangedCanvas struct { ExtensionID string `json:"extensionId"` // Owning extension display name, when available ExtensionName *string `json:"extensionName,omitempty"` + // Host-local PNG path for the canvas icon, when supplied + Icon *string `json:"icon,omitempty"` // JSON Schema for canvas open input InputSchema any `json:"inputSchema,omitempty"` } @@ -2236,6 +2375,14 @@ type HandoffRepository struct { Owner string `json:"owner"` } +// Single HTTP header entry as a name/value pair. +type HeaderEntry struct { + // HTTP response header name as observed by the runtime. + Name string `json:"name"` + // HTTP response header value as observed by the runtime. + Value string `json:"value"` +} + // Error details when the hook failed type HookEndError struct { // Human-readable error message @@ -2266,6 +2413,16 @@ type MCPAppToolCallCompleteToolMetaUI struct { Visibility []string `json:"visibility,omitzero"` } +// Raw HTTP response details from the OAuth auth challenge, as observed by the runtime. +type MCPOauthHTTPResponse struct { + // Complete UTF-8 response body for host-specific challenge handling, including an empty string for an empty body. Omitted when the complete body is not valid UTF-8; body read failures fail the HTTP operation rather than exposing a partial response. + Body *string `json:"body,omitempty"` + // HTTP response headers as observed by the runtime. Order and casing are transport-dependent, and duplicate header names may appear multiple times. + Headers []HeaderEntry `json:"headers"` + // HTTP status code returned with the auth challenge. + StatusCode int32 `json:"statusCode"` +} + // Static OAuth client configuration, if the server specifies one type MCPOauthRequiredStaticClientConfig struct { // OAuth client ID for the server @@ -2540,6 +2697,10 @@ type PermissionPromptRequestURL struct { AutoApproval *PermissionAutoApproval `json:"autoApproval,omitempty"` // Human-readable description of why the URL is being accessed Intention string `json:"intention"` + // 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. + RequestSandboxBypass *bool `json:"requestSandboxBypass,omitempty"` + // Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + RequestSandboxBypassReason *string `json:"requestSandboxBypassReason,omitempty"` // Tool call ID that triggered this permission request ToolCallID *string `json:"toolCallId,omitempty"` // URL to be fetched @@ -2753,6 +2914,10 @@ func (PermissionRequestShell) Kind() PermissionRequestKind { type PermissionRequestURL struct { // Human-readable description of why the URL is being accessed Intention string `json:"intention"` + // 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. + RequestSandboxBypass *bool `json:"requestSandboxBypass,omitempty"` + // Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + RequestSandboxBypassReason *string `json:"requestSandboxBypassReason,omitempty"` // Tool call ID that triggered this permission request ToolCallID *string `json:"toolCallId,omitempty"` // URL to be fetched @@ -2776,6 +2941,10 @@ type PermissionRequestWrite struct { Intention string `json:"intention"` // Complete new file contents for newly created files NewFileContents *string `json:"newFileContents,omitempty"` + // True when a built-in file tool (apply_patch / str_replace_editor) asked to write a path the sandbox filesystem policy would block, and the host opted in via sandbox.allowBypass. This is a request, not a grant: the write happens unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + RequestSandboxBypass *bool `json:"requestSandboxBypass,omitempty"` + // Justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + RequestSandboxBypassReason *string `json:"requestSandboxBypassReason,omitempty"` // Tool call ID that triggered this permission request ToolCallID *string `json:"toolCallId,omitempty"` } @@ -3370,6 +3539,9 @@ type ToolExecutionCompleteResult struct { Contents []ToolExecutionCompleteContent `json:"contents,omitzero"` // Full detailed tool result for UI/timeline display, preserving complete content such as diffs. Falls back to content when absent. DetailedContent *string `json:"detailedContent,omitempty"` + // FIDES IFC label projected from tool ingress metadata (MCP `CallToolResult._meta` or synthesized built-in ingress labels) — persisted as `{ ifc: ... }` (only the `ifc` key, not the whole `_meta`). Persisted so the FIDES IFC label survives session resume: the engine rehydrates accumulated taint by replaying these on load. Populated for ingress sources when FIDES IFC is on. Experimental. + // Experimental: MCPMeta is part of an experimental API and may change or be removed. + MCPMeta any `json:"mcpMeta,omitempty"` // Structured content (arbitrary JSON) returned verbatim by the MCP tool StructuredContent any `json:"structuredContent,omitempty"` // MCP Apps UI resource content for rendering in a sandboxed iframe @@ -3557,6 +3729,18 @@ const ( AutoApprovalRecommendationRequireApproval AutoApprovalRecommendation = "requireApproval" ) +// Coarse request-difficulty bucket for UX explainability +type AutoModeResolvedReasoningBucket string + +const ( + // The request looks high-reasoning; a stronger model is appropriate. + AutoModeResolvedReasoningBucketHigh AutoModeResolvedReasoningBucket = "high" + // The request looks low-reasoning; a lighter model is appropriate. + AutoModeResolvedReasoningBucketLow AutoModeResolvedReasoningBucket = "low" + // The request needs a moderate amount of reasoning. + AutoModeResolvedReasoningBucketMedium AutoModeResolvedReasoningBucket = "medium" +) + // The user's auto-mode-switch choice type AutoModeSwitchResponse string @@ -3719,6 +3903,18 @@ const ( HandoffSourceTypeRemote HandoffSourceType = "remote" ) +// Which channel supplied the effective enterprise managed settings (highest-authority present layer wins wholesale) +type ManagedSettingsResolvedSource string + +const ( + // Device-level MDM policy discovered from plist/registry/file (lower authority). + ManagedSettingsResolvedSourceDevice ManagedSettingsResolvedSource = "device" + // No managed policy is in force (no layer contributed). + ManagedSettingsResolvedSourceNone ManagedSettingsResolvedSource = "none" + // Account/org policy self-fetched from the GitHub managed-settings endpoint (higher authority). + ManagedSettingsResolvedSourceServer ManagedSettingsResolvedSource = "server" +) + // How the pending MCP headers refresh request resolved. type MCPHeadersRefreshCompletedOutcome string diff --git a/go/session.go b/go/session.go index 808876761e..c4e742e906 100644 --- a/go/session.go +++ b/go/session.go @@ -13,6 +13,11 @@ import ( "github.com/github/copilot-sdk/go/rpc" ) +// toolSearchToolName is the fixed name of the runtime's built-in tool-search +// tool. A client can replace its behavior by registering a [Tool] with this +// exact name and OverridesBuiltInTool set to true. +const toolSearchToolName = "tool_search_tool" + type sessionHandler struct { id uint64 fn SessionEventHandler @@ -163,6 +168,7 @@ func (s *Session) updateOpenCanvasesFromEvent(event SessionEvent) { InstanceID: data.InstanceID, Status: data.Status, Title: data.Title, + Icon: data.Icon, URL: data.URL, }) case *SessionCanvasClosedData: @@ -1513,6 +1519,17 @@ func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string, TraceContext: ctx, } + // The built-in tool-search tool receives a snapshot of the session's + // currently initialized tools so an override can filter the live catalog + // without issuing its own RPC. Fetch it only for that tool to avoid a + // round-trip on every tool call; a failed fetch leaves the snapshot nil + // rather than failing the tool. + if toolName == toolSearchToolName { + if metadata, mErr := s.RPC.Tools.GetCurrentMetadata(ctx); mErr == nil && metadata != nil { + invocation.AvailableTools = metadata.Tools + } + } + result, err := handler(invocation) if err != nil { errMsg := err.Error() @@ -1542,6 +1559,7 @@ func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string, TextResultForLlm: textResultForLLM, ToolTelemetry: result.ToolTelemetry, ResultType: &effectiveResultType, + ToolReferences: result.ToolReferences, } if result.Error != "" { rpcResult.Error = &result.Error @@ -1735,7 +1753,7 @@ type SetModelOptions struct { // // Example: // -// if err := session.SetModel(context.Background(), "gpt-4.1", nil); err != nil { +// if err := session.SetModel(context.Background(), "gpt-5.4", nil); err != nil { // log.Printf("Failed to set model: %v", err) // } // if err := session.SetModel(context.Background(), "claude-sonnet-4.6", &SetModelOptions{ReasoningEffort: new("high")}); err != nil { diff --git a/go/session_event_serialization_test.go b/go/session_event_serialization_test.go index 5f8855336c..bd47fdfbe2 100644 --- a/go/session_event_serialization_test.go +++ b/go/session_event_serialization_test.go @@ -145,6 +145,26 @@ func TestSessionEventAgentIDRoundTripsUnknownEvent(t *testing.T) { } } +func TestInternalSessionEventUsesRawFallback(t *testing.T) { + var event SessionEvent + if err := json.Unmarshal([]byte(`{ + "id": "00000000-0000-0000-0000-000000000003", + "timestamp": "2026-01-01T00:00:00Z", + "parentId": null, + "type": "session.memory_changed", + "data": {} + }`), &event); err != nil { + t.Fatalf("failed to unmarshal internal session event: %v", err) + } + + if _, ok := event.Data.(*RawSessionEventData); !ok { + t.Fatalf("expected internal event to use raw session event data, got %T", event.Data) + } + if event.Type() != "session.memory_changed" { + t.Fatalf("expected internal event type to be preserved, got %q", event.Type()) + } +} + func TestRawSessionEventDataWithNilRawMarshalsAsNull(t *testing.T) { event := SessionEvent{ Data: &RawSessionEventData{EventType: "future.event"}, diff --git a/go/session_test.go b/go/session_test.go index 277ea29e3e..d34c34233b 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -793,6 +793,7 @@ func TestSession_Capabilities(t *testing.T) { CanvasID: "counter", InstanceID: "counter-1", Title: ptr("Counter"), + Icon: ptr("beaker"), Status: ptr("ready"), URL: ptr("https://example.test/counter"), Input: map[string]any{"seed": float64(1)}, @@ -822,6 +823,7 @@ func TestSession_Capabilities(t *testing.T) { CanvasID: "counter", InstanceID: "counter-1", Title: ptr("Counter Updated"), + Icon: ptr("beaker-filled"), Status: ptr("reconnected"), URL: ptr("https://example.test/counter-updated"), Input: map[string]any{"seed": float64(2)}, @@ -838,6 +840,9 @@ func TestSession_Capabilities(t *testing.T) { if open[0].Title == nil || *open[0].Title != "Counter Updated" { t.Fatalf("expected updated title, got %+v", open[0].Title) } + if open[0].Icon == nil || *open[0].Icon != "beaker-filled" { + t.Fatalf("expected updated icon, got %+v", open[0].Icon) + } if open[0].Status == nil || *open[0].Status != "reconnected" { t.Fatalf("expected updated status, got %+v", open[0].Status) } diff --git a/go/types.go b/go/types.go index 1e424514eb..6760f25e07 100644 --- a/go/types.go +++ b/go/types.go @@ -20,14 +20,23 @@ const ( // RuntimeConnection describes how a [Client] connects to the Copilot runtime. // -// Construct one with a [StdioConnection], [TCPConnection], or [URIConnection] -// literal and pass it via [ClientOptions.Connection]. When [ClientOptions.Connection] -// is nil, the default is an empty [StdioConnection] (the SDK spawns the bundled -// runtime and communicates over stdin/stdout). +// Construct one with a [StdioConnection], [TCPConnection], [URIConnection], or +// [InProcessConnection] literal and pass it via [ClientOptions.Connection]. When +// [ClientOptions.Connection] is nil, COPILOT_SDK_DEFAULT_CONNECTION may select +// "inprocess" or "stdio"; when unset, the default is an empty [StdioConnection]. type RuntimeConnection interface { runtimeConnection() } +// childProcessConnection is implemented by the connection types that spawn a +// runtime child process ([StdioConnection] and [TCPConnection]). It exposes the +// per-connection environment so the client can resolve and validate it uniformly +// regardless of the specific child-process transport. +type childProcessConnection interface { + RuntimeConnection + connEnv() []string +} + // StdioConnection spawns a runtime child process and communicates over its // stdin/stdout pipes. This is the default when no connection is configured. type StdioConnection struct { @@ -35,10 +44,17 @@ type StdioConnection struct { Path string // Args are extra command-line arguments inserted before SDK-managed args. Args []string + // Env are the environment variables for the runtime process, each of the + // form "KEY=VALUE". When set, these take precedence over + // [ClientOptions.Env]; setting both is rejected. When nil, the client-level + // env (or the current process environment) is used. + Env []string } func (StdioConnection) runtimeConnection() {} +func (c StdioConnection) connEnv() []string { return c.Env } + // TCPConnection spawns a runtime child process that listens on a TCP socket // and connects to it. type TCPConnection struct { @@ -54,10 +70,17 @@ type TCPConnection struct { Path string // Args are extra command-line arguments inserted before SDK-managed args. Args []string + // Env are the environment variables for the runtime process, each of the + // form "KEY=VALUE". When set, these take precedence over + // [ClientOptions.Env]; setting both is rejected. When nil, the client-level + // env (or the current process environment) is used. + Env []string } func (TCPConnection) runtimeConnection() {} +func (c TCPConnection) connEnv() []string { return c.Env } + // URIConnection connects to an already-running runtime at the given URL. // The SDK does not spawn a process in this mode. type URIConnection struct { @@ -71,11 +94,29 @@ type URIConnection struct { func (URIConnection) runtimeConnection() {} +// InProcessConnection hosts the Copilot runtime in-process by loading its native +// runtime library (a Rust cdylib) and driving JSON-RPC over the library's C ABI, +// instead of spawning a runtime child process. +// +// Because the runtime is loaded into the calling process, per-client +// environment, working directory, and telemetry cannot be represented and are +// rejected by [NewClient] (see [ClientOptions]). Set those via the host process +// environment instead, or use a child-process transport ([StdioConnection] / +// [TCPConnection]). +// +// Experimental: the in-process transport is experimental and its API and +// behavior may change in a future release. Build the application with the +// copilot_inprocess build tag to enable this transport. +type InProcessConnection struct { +} + +func (InProcessConnection) runtimeConnection() {} + // ClientOptions configures the [Client]. type ClientOptions struct { // Connection describes how to connect to the Copilot runtime. When nil, - // defaults to an empty [StdioConnection] (spawn the bundled runtime over - // stdio). + // COPILOT_SDK_DEFAULT_CONNECTION may select "inprocess" or "stdio"; + // when unset, defaults to an empty [StdioConnection]. Connection RuntimeConnection // WorkingDirectory is the working directory for the runtime process. // If empty, inherits the current process's working directory. @@ -95,6 +136,12 @@ type ClientOptions struct { // Env are the environment variables for the runtime process (default: // inherits from current process). Each entry is of the form "KEY=VALUE". // If Env contains duplicate keys, only the last value for each key is used. + // + // For child-process transports ([StdioConnection] / [TCPConnection]) the + // per-connection Env, when set, takes precedence over this field; setting + // both is rejected. Env is not supported with [InProcessConnection] (the + // runtime shares this process's single environment block) and is rejected + // by [NewClient]. Env []string // GitHubToken is the GitHub token to use for authentication. // When provided, the token is passed to the runtime via environment @@ -902,6 +949,10 @@ type CustomAgentConfig struct { // When set, the runtime will attempt to use this model for the agent, // falling back to the parent session model if unavailable. Model string `json:"model,omitempty"` + // ReasoningEffort is the reasoning effort level for this agent's model. + // When empty, no per-agent override is sent and the backend chooses its + // default. The parent session effort is not inherited. + ReasoningEffort string `json:"reasoningEffort,omitempty"` } // DefaultAgentConfig configures the default agent (the built-in agent that handles turns when no custom agent is selected). @@ -948,6 +999,18 @@ type LargeToolOutputConfig struct { OutputDirectory string `json:"outputDir,omitempty"` } +// ToolSearchConfig allows to configure tool search behavior. +// Tool search defers tools to keep the model's active tool set small. +// To override the tool-search tool's implementation, register a +// [Tool] named "tool_search_tool" with OverridesBuiltInTool set to true. +type ToolSearchConfig struct { + // Controls whether tool search is enabled. + Enabled *bool `json:"enabled,omitempty"` + // DeferThreshold is the tool count above which MCP and external tools are + // deferred behind tool search. When nil, the runtime default (30) applies. + DeferThreshold *int `json:"deferThreshold,omitempty"` +} + // SessionFSCapabilities declares optional provider capabilities. type SessionFSCapabilities struct { // Sqlite indicates whether the provider supports SQLite query/exists operations. @@ -1153,6 +1216,10 @@ type SessionConfig struct { // output exceeding the configured size, the output is written to a temp file // and a reference is returned to the model instead of the full payload. LargeOutput *LargeToolOutputConfig + // ToolSearch overrides the runtime's built-in tool-search behavior, which + // defers rarely used tools behind a searchable index. When nil, the runtime + // default applies. + ToolSearch *ToolSearchConfig // Memory configures the memory feature for the session. When omitted, the // runtime default applies. Memory *MemoryConfiguration @@ -1235,6 +1302,9 @@ type SessionConfig struct { CanvasHandler CanvasHandler `json:"-"` // ExtensionInfo identifies the stable extension providing this session's canvases. ExtensionInfo *ExtensionInfo + // CanvasProvider is the stable identity for a host/SDK connection that + // supplies built-in canvases, so they survive reconnect and CLI restart. + CanvasProvider *CanvasProviderIdentity // ExpAssignments injects ExP assignment ("flight") data for this session, // in the same JSON shape the Copilot CLI fetches from the experimentation // service (CopilotExpAssignmentResponse). When supplied, the runtime feeds @@ -1247,6 +1317,12 @@ type SessionConfig struct { // intended for trusted out-of-process integrators, and is not intended for // general external use. ExpAssignments any + // EnableManagedSettings, when set to true, opts the runtime into + // self-fetching enterprise managed settings (bypass-permissions policy) at + // session bootstrap using the session's GitHubToken. Requires GitHubToken to + // be set; if omitted, the runtime is expected to reject session creation + // (fail-closed). Unset behaves exactly as before. + EnableManagedSettings *bool } // ToolDefer controls whether a tool may be deferred (loaded lazily via tool @@ -1269,6 +1345,11 @@ type Tool struct { // Defer controls whether the tool may be deferred (loaded lazily via tool // search) rather than always pre-loaded. When empty, the runtime decides. Defer ToolDefer `json:"defer,omitempty"` + // Metadata is opaque, host-defined metadata associated with the tool + // definition. Keys are namespaced and not part of the stable public API; + // values are not interpreted and may be recognized to inform host-specific + // behavior. Unknown keys are preserved and round-tripped untouched. + Metadata map[string]any `json:"metadata,omitempty"` // Handler is optional. When nil, the SDK exposes the tool declaration but does // not automatically invoke it. Handler ToolHandler `json:"-"` @@ -1281,6 +1362,14 @@ type ToolInvocation struct { ToolName string Arguments any + // AvailableTools is a snapshot of the session's currently initialized + // tools. The SDK populates it only when this invocation targets the + // built-in tool-search tool ("tool_search_tool"), so a tool-search + // override can rank/filter the live catalog -- including MCP tools + // configured in settings -- without issuing its own RPC. It is nil for + // every other tool invocation. + AvailableTools []rpc.CurrentToolMetadata + // 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. @@ -1300,6 +1389,8 @@ type ToolResult struct { Error string `json:"error,omitempty"` SessionLog string `json:"sessionLog,omitempty"` ToolTelemetry map[string]any `json:"toolTelemetry,omitempty"` + // ToolReferences lists names of tools returned by a tool-search tool. + ToolReferences []string `json:"toolReferences,omitempty"` } // CommandContext provides context about a slash-command invocation. @@ -1596,6 +1687,10 @@ type ResumeSessionConfig struct { // output exceeding the configured size, the output is written to a temp file // and a reference is returned to the model instead of the full payload. LargeOutput *LargeToolOutputConfig + // ToolSearch overrides the runtime's built-in tool-search behavior, which + // defers rarely used tools behind a searchable index. When nil, the runtime + // default applies. + ToolSearch *ToolSearchConfig // Memory configures the memory feature for the session. When omitted, the // runtime default applies. Memory *MemoryConfiguration @@ -1660,6 +1755,9 @@ type ResumeSessionConfig struct { CanvasHandler CanvasHandler `json:"-"` // ExtensionInfo identifies the stable extension providing this session's canvases. ExtensionInfo *ExtensionInfo + // CanvasProvider is the stable identity for a host/SDK connection that + // supplies built-in canvases. See SessionConfig.CanvasProvider. + CanvasProvider *CanvasProviderIdentity // ExpAssignments injects ExP assignment ("flight") data on resume. See // SessionConfig.ExpAssignments. Re-supply on resume so the runtime // re-applies the assignments after a CLI process restart. @@ -1668,6 +1766,10 @@ type ResumeSessionConfig struct { // intended for trusted out-of-process integrators, and is not intended for // general external use. ExpAssignments any + // EnableManagedSettings injects the same opt-in flag on resume. See + // SessionConfig.EnableManagedSettings. Re-supply on resume so the runtime + // re-applies the managed-settings self-fetch after a CLI process restart. + EnableManagedSettings *bool } // ProviderTokenArgs carries the context passed to a [BearerTokenProvider] callback @@ -2109,6 +2211,7 @@ type createSessionRequest struct { DisabledSkills []string `json:"disabledSkills,omitempty"` InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` LargeOutput *LargeToolOutputConfig `json:"largeOutput,omitempty"` + ToolSearch *ToolSearchConfig `json:"toolSearch,omitempty"` Memory *MemoryConfiguration `json:"memory,omitempty"` Commands []wireCommand `json:"commands,omitempty"` RequestElicitation *bool `json:"requestElicitation,omitempty"` @@ -2121,7 +2224,9 @@ type createSessionRequest struct { RequestExtensions *bool `json:"requestExtensions,omitempty"` ExtensionSDKPath *string `json:"extensionSdkPath,omitempty"` ExtensionInfo *ExtensionInfo `json:"extensionInfo,omitempty"` + CanvasProvider *CanvasProviderIdentity `json:"canvasProvider,omitempty"` ExpAssignments any `json:"expAssignments,omitempty"` + EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` Traceparent string `json:"traceparent,omitempty"` Tracestate string `json:"tracestate,omitempty"` } @@ -2198,6 +2303,7 @@ type resumeSessionRequest struct { DisabledSkills []string `json:"disabledSkills,omitempty"` InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` LargeOutput *LargeToolOutputConfig `json:"largeOutput,omitempty"` + ToolSearch *ToolSearchConfig `json:"toolSearch,omitempty"` Memory *MemoryConfiguration `json:"memory,omitempty"` Commands []wireCommand `json:"commands,omitempty"` RequestElicitation *bool `json:"requestElicitation,omitempty"` @@ -2210,7 +2316,9 @@ type resumeSessionRequest struct { RequestExtensions *bool `json:"requestExtensions,omitempty"` ExtensionSDKPath *string `json:"extensionSdkPath,omitempty"` ExtensionInfo *ExtensionInfo `json:"extensionInfo,omitempty"` + CanvasProvider *CanvasProviderIdentity `json:"canvasProvider,omitempty"` ExpAssignments any `json:"expAssignments,omitempty"` + EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` Traceparent string `json:"traceparent,omitempty"` Tracestate string `json:"tracestate,omitempty"` } diff --git a/go/types_test.go b/go/types_test.go index f4132fa3d6..a76ebaad40 100644 --- a/go/types_test.go +++ b/go/types_test.go @@ -152,6 +152,28 @@ func TestCustomAgentConfig_JSONIncludesModel(t *testing.T) { } } +func TestCustomAgentConfig_JSONIncludesReasoningEffort(t *testing.T) { + cfg := CustomAgentConfig{ + Name: "reasoning-agent", + Prompt: "Think carefully.", + ReasoningEffort: "high", + } + + data, err := json.Marshal(cfg) + if err != nil { + t.Fatalf("failed to marshal CustomAgentConfig: %v", err) + } + + var decoded map[string]any + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("failed to unmarshal CustomAgentConfig: %v", err) + } + + if decoded["reasoningEffort"] != "high" { + t.Errorf("expected reasoningEffort 'high', got %v", decoded["reasoningEffort"]) + } +} + func TestCustomAgentConfig_JSONIncludesEmptyTools(t *testing.T) { cfg := CustomAgentConfig{ Name: "no-tools-agent", @@ -203,6 +225,57 @@ func TestCustomAgentConfig_JSONOmitsNilTools(t *testing.T) { } } +func TestToolResult_JSONIncludesToolReferences(t *testing.T) { + result := ToolResult{ + TextResultForLLM: "found 2 tools", + ResultType: "success", + ToolReferences: []string{"get_weather", "check_status"}, + } + + data, err := json.Marshal(result) + if err != nil { + t.Fatalf("failed to marshal ToolResult: %v", err) + } + + var decoded map[string]any + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("failed to unmarshal ToolResult: %v", err) + } + + rawRefs, present := decoded["toolReferences"] + if !present { + t.Fatal("expected toolReferences to be present") + } + refs, ok := rawRefs.([]any) + if !ok { + t.Fatalf("expected toolReferences array, got %T", rawRefs) + } + if len(refs) != 2 || refs[0] != "get_weather" || refs[1] != "check_status" { + t.Errorf("unexpected toolReferences: %v", refs) + } +} + +func TestToolResult_JSONOmitsNilToolReferences(t *testing.T) { + result := ToolResult{ + TextResultForLLM: "ok", + ResultType: "success", + } + + data, err := json.Marshal(result) + if err != nil { + t.Fatalf("failed to marshal ToolResult: %v", err) + } + + var decoded map[string]any + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("failed to unmarshal ToolResult: %v", err) + } + + if _, present := decoded["toolReferences"]; present { + t.Errorf("expected toolReferences to be omitted for nil slice, got %v", decoded["toolReferences"]) + } +} + func TestCustomAgentConfig_JSONOmitsModelWhenEmpty(t *testing.T) { cfg := CustomAgentConfig{ Name: "no-model-agent", @@ -222,6 +295,9 @@ func TestCustomAgentConfig_JSONOmitsModelWhenEmpty(t *testing.T) { if _, present := decoded["model"]; present { t.Errorf("expected model to be omitted when empty, got %v", decoded["model"]) } + if _, present := decoded["reasoningEffort"]; present { + t.Errorf("expected reasoningEffort to be omitted when empty, got %v", decoded["reasoningEffort"]) + } } func TestTool_JSONIncludesEmptyParameters(t *testing.T) { diff --git a/go/zsession_events.go b/go/zsession_events.go index 04756c8af2..b3dd66ef77 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -19,7 +19,9 @@ type ( AssistantMessageToolRequestType = rpc.AssistantMessageToolRequestType AssistantReasoningData = rpc.AssistantReasoningData AssistantReasoningDeltaData = rpc.AssistantReasoningDeltaData + AssistantServerToolProgressData = rpc.AssistantServerToolProgressData AssistantStreamingDeltaData = rpc.AssistantStreamingDeltaData + AssistantToolCallDeltaData = rpc.AssistantToolCallDeltaData AssistantTurnEndData = rpc.AssistantTurnEndData AssistantTurnStartData = rpc.AssistantTurnStartData AssistantUsageAPIEndpoint = rpc.AssistantUsageAPIEndpoint @@ -51,6 +53,7 @@ type ( AttachmentSelectionDetailsStart = rpc.AttachmentSelectionDetailsStart AttachmentType = rpc.AttachmentType AutoApprovalRecommendation = rpc.AutoApprovalRecommendation + AutoModeResolvedReasoningBucket = rpc.AutoModeResolvedReasoningBucket AutoModeSwitchCompletedData = rpc.AutoModeSwitchCompletedData AutoModeSwitchRequestedData = rpc.AutoModeSwitchRequestedData AutoModeSwitchResponse = rpc.AutoModeSwitchResponse @@ -102,10 +105,12 @@ type ( GitHubRepoRef = rpc.GitHubRepoRef HandoffRepository = rpc.HandoffRepository HandoffSourceType = rpc.HandoffSourceType + HeaderEntry = rpc.HeaderEntry HookEndData = rpc.HookEndData HookEndError = rpc.HookEndError HookProgressData = rpc.HookProgressData HookStartData = rpc.HookStartData + ManagedSettingsResolvedSource = rpc.ManagedSettingsResolvedSource MCPAppToolCallCompleteData = rpc.MCPAppToolCallCompleteData MCPAppToolCallCompleteError = rpc.MCPAppToolCallCompleteError MCPAppToolCallCompleteToolMeta = rpc.MCPAppToolCallCompleteToolMeta @@ -116,15 +121,19 @@ type ( MCPHeadersRefreshRequiredReason = rpc.MCPHeadersRefreshRequiredReason MCPOauthCompletedData = rpc.MCPOauthCompletedData MCPOauthCompletionOutcome = rpc.MCPOauthCompletionOutcome + MCPOauthHTTPResponse = rpc.MCPOauthHTTPResponse MCPOauthRequestReason = rpc.MCPOauthRequestReason MCPOauthRequiredData = rpc.MCPOauthRequiredData MCPOauthRequiredStaticClientConfig = rpc.MCPOauthRequiredStaticClientConfig MCPOauthRequiredStaticClientConfigGrantType = rpc.MCPOauthRequiredStaticClientConfigGrantType MCPOauthWwwAuthenticateParams = rpc.MCPOauthWwwAuthenticateParams + MCPPromptsListChangedData = rpc.MCPPromptsListChangedData + MCPResourcesListChangedData = rpc.MCPResourcesListChangedData MCPServersLoadedServer = rpc.MCPServersLoadedServer MCPServerSource = rpc.MCPServerSource MCPServerStatus = rpc.MCPServerStatus MCPServerTransport = rpc.MCPServerTransport + MCPToolsListChangedData = rpc.MCPToolsListChangedData ModelCallFailureBadRequestKind = rpc.ModelCallFailureBadRequestKind ModelCallFailureData = rpc.ModelCallFailureData ModelCallFailureRequestFingerprint = rpc.ModelCallFailureRequestFingerprint @@ -197,6 +206,7 @@ type ( ReasoningSummary = rpc.ReasoningSummary SamplingCompletedData = rpc.SamplingCompletedData SamplingRequestedData = rpc.SamplingRequestedData + SessionAutoModeResolvedData = rpc.SessionAutoModeResolvedData SessionAutopilotObjectiveChangedData = rpc.SessionAutopilotObjectiveChangedData SessionBackgroundTasksChangedData = rpc.SessionBackgroundTasksChangedData SessionBinaryAssetData = rpc.SessionBinaryAssetData @@ -225,6 +235,7 @@ type ( SessionLimitsExhaustedRequestedData = rpc.SessionLimitsExhaustedRequestedData SessionLimitsExhaustedResponse = rpc.SessionLimitsExhaustedResponse SessionLimitsExhaustedResponseAction = rpc.SessionLimitsExhaustedResponseAction + SessionManagedSettingsResolvedData = rpc.SessionManagedSettingsResolvedData SessionMCPServersLoadedData = rpc.SessionMCPServersLoadedData SessionMCPServerStatusChangedData = rpc.SessionMCPServerStatusChangedData SessionMode = rpc.SessionMode @@ -332,6 +343,7 @@ type ( UserToolSessionApprovalMemory = rpc.UserToolSessionApprovalMemory UserToolSessionApprovalRead = rpc.UserToolSessionApprovalRead UserToolSessionApprovalWrite = rpc.UserToolSessionApprovalWrite + Verbosity = rpc.Verbosity WorkingDirectoryContext = rpc.WorkingDirectoryContext WorkingDirectoryContextHostType = rpc.WorkingDirectoryContextHostType WorkspaceFileChangedOperation = rpc.WorkspaceFileChangedOperation @@ -370,6 +382,9 @@ const ( AutoApprovalRecommendationError = rpc.AutoApprovalRecommendationError AutoApprovalRecommendationExcluded = rpc.AutoApprovalRecommendationExcluded AutoApprovalRecommendationRequireApproval = rpc.AutoApprovalRecommendationRequireApproval + AutoModeResolvedReasoningBucketHigh = rpc.AutoModeResolvedReasoningBucketHigh + AutoModeResolvedReasoningBucketLow = rpc.AutoModeResolvedReasoningBucketLow + AutoModeResolvedReasoningBucketMedium = rpc.AutoModeResolvedReasoningBucketMedium AutoModeSwitchResponseNo = rpc.AutoModeSwitchResponseNo AutoModeSwitchResponseYes = rpc.AutoModeSwitchResponseYes AutoModeSwitchResponseYesAlways = rpc.AutoModeSwitchResponseYesAlways @@ -412,6 +427,9 @@ const ( ExtensionsLoadedExtensionStatusStarting = rpc.ExtensionsLoadedExtensionStatusStarting HandoffSourceTypeLocal = rpc.HandoffSourceTypeLocal HandoffSourceTypeRemote = rpc.HandoffSourceTypeRemote + ManagedSettingsResolvedSourceDevice = rpc.ManagedSettingsResolvedSourceDevice + ManagedSettingsResolvedSourceNone = rpc.ManagedSettingsResolvedSourceNone + ManagedSettingsResolvedSourceServer = rpc.ManagedSettingsResolvedSourceServer MCPHeadersRefreshCompletedOutcomeHeaders = rpc.MCPHeadersRefreshCompletedOutcomeHeaders MCPHeadersRefreshCompletedOutcomeNone = rpc.MCPHeadersRefreshCompletedOutcomeNone MCPHeadersRefreshCompletedOutcomeTimeout = rpc.MCPHeadersRefreshCompletedOutcomeTimeout @@ -506,7 +524,9 @@ const ( SessionEventTypeAssistantMessageStart = rpc.SessionEventTypeAssistantMessageStart SessionEventTypeAssistantReasoning = rpc.SessionEventTypeAssistantReasoning SessionEventTypeAssistantReasoningDelta = rpc.SessionEventTypeAssistantReasoningDelta + SessionEventTypeAssistantServerToolProgress = rpc.SessionEventTypeAssistantServerToolProgress SessionEventTypeAssistantStreamingDelta = rpc.SessionEventTypeAssistantStreamingDelta + SessionEventTypeAssistantToolCallDelta = rpc.SessionEventTypeAssistantToolCallDelta SessionEventTypeAssistantTurnEnd = rpc.SessionEventTypeAssistantTurnEnd SessionEventTypeAssistantTurnStart = rpc.SessionEventTypeAssistantTurnStart SessionEventTypeAssistantUsage = rpc.SessionEventTypeAssistantUsage @@ -531,12 +551,16 @@ const ( SessionEventTypeMCPHeadersRefreshRequired = rpc.SessionEventTypeMCPHeadersRefreshRequired SessionEventTypeMCPOauthCompleted = rpc.SessionEventTypeMCPOauthCompleted SessionEventTypeMCPOauthRequired = rpc.SessionEventTypeMCPOauthRequired + SessionEventTypeMCPPromptsListChanged = rpc.SessionEventTypeMCPPromptsListChanged + SessionEventTypeMCPResourcesListChanged = rpc.SessionEventTypeMCPResourcesListChanged + SessionEventTypeMCPToolsListChanged = rpc.SessionEventTypeMCPToolsListChanged SessionEventTypeModelCallFailure = rpc.SessionEventTypeModelCallFailure SessionEventTypePendingMessagesModified = rpc.SessionEventTypePendingMessagesModified SessionEventTypePermissionCompleted = rpc.SessionEventTypePermissionCompleted SessionEventTypePermissionRequested = rpc.SessionEventTypePermissionRequested SessionEventTypeSamplingCompleted = rpc.SessionEventTypeSamplingCompleted SessionEventTypeSamplingRequested = rpc.SessionEventTypeSamplingRequested + SessionEventTypeSessionAutoModeResolved = rpc.SessionEventTypeSessionAutoModeResolved SessionEventTypeSessionAutopilotObjectiveChanged = rpc.SessionEventTypeSessionAutopilotObjectiveChanged SessionEventTypeSessionBackgroundTasksChanged = rpc.SessionEventTypeSessionBackgroundTasksChanged SessionEventTypeSessionBinaryAsset = rpc.SessionEventTypeSessionBinaryAsset @@ -559,6 +583,7 @@ const ( SessionEventTypeSessionInfo = rpc.SessionEventTypeSessionInfo SessionEventTypeSessionLimitsExhaustedCompleted = rpc.SessionEventTypeSessionLimitsExhaustedCompleted SessionEventTypeSessionLimitsExhaustedRequested = rpc.SessionEventTypeSessionLimitsExhaustedRequested + SessionEventTypeSessionManagedSettingsResolved = rpc.SessionEventTypeSessionManagedSettingsResolved SessionEventTypeSessionMCPServersLoaded = rpc.SessionEventTypeSessionMCPServersLoaded SessionEventTypeSessionMCPServerStatusChanged = rpc.SessionEventTypeSessionMCPServerStatusChanged SessionEventTypeSessionModeChanged = rpc.SessionEventTypeSessionModeChanged @@ -657,6 +682,9 @@ const ( UserToolSessionApprovalKindMemory = rpc.UserToolSessionApprovalKindMemory UserToolSessionApprovalKindRead = rpc.UserToolSessionApprovalKindRead UserToolSessionApprovalKindWrite = rpc.UserToolSessionApprovalKindWrite + VerbosityHigh = rpc.VerbosityHigh + VerbosityLow = rpc.VerbosityLow + VerbosityMedium = rpc.VerbosityMedium WorkingDirectoryContextHostTypeADO = rpc.WorkingDirectoryContextHostTypeADO WorkingDirectoryContextHostTypeGitHub = rpc.WorkingDirectoryContextHostTypeGitHub WorkspaceFileChangedOperationCreate = rpc.WorkspaceFileChangedOperationCreate diff --git a/java/README.md b/java/README.md index 334db459f5..525cb364dd 100644 --- a/java/README.md +++ b/java/README.md @@ -39,7 +39,7 @@ Replace `${copilot.sdk.version}` with the latest release from Maven Central. ### Gradle ```groovy -implementation 'com.github:copilot-sdk-java:1.0.6-preview.1-01' +implementation 'com.github:copilot-sdk-java:1.0.7-01' ``` #### Snapshot Builds @@ -58,7 +58,7 @@ Snapshot builds of the next development version are published to Maven Central S com.github copilot-sdk-java - 1.0.7-SNAPSHOT + 1.0.8-SNAPSHOT ``` @@ -67,7 +67,7 @@ Snapshot builds of the next development version are published to Maven Central S Replace `${copilot.sdk.version}` with the latest release from Maven Central. ```groovy -implementation 'com.github:copilot-sdk-java:1.0.6-preview.1-01-SNAPSHOT' +implementation 'com.github:copilot-sdk-java:1.0.7-01-SNAPSHOT' ``` ## Quick Start diff --git a/java/docs/adr/adr-005-tool-definition.md b/java/docs/adr/adr-005-tool-definition.md index e1b4dcc764..dc1eb36143 100644 --- a/java/docs/adr/adr-005-tool-definition.md +++ b/java/docs/adr/adr-005-tool-definition.md @@ -181,14 +181,45 @@ final class MyTools$$CopilotToolMeta { Phase phase = invocation.getArgumentsAs(Phase.class); return CompletableFuture.completedFuture( instance.setCurrentPhase(phase)); - }, null, null, null) + }, null, null, null, null) ); } } ``` +The trailing constructor arguments are `overridesBuiltInTool`, `skipPermission`, `defer`, and `metadata` — all `null` here because none were set on the annotation. + At runtime, `ToolDefinition.fromObject(myTools)` loads the generated `$$CopilotToolMeta` class — zero reflection, zero dependency on `-parameters`. +### Host-defined metadata + +`@CopilotTool` also accepts an opaque `metadata` bag via nested annotations. Because annotation members can't express arbitrary maps, the representation is deliberately shallow: each entry maps a namespaced key to a boolean, a string, or a one-level map of named boolean flags. + +```java +@CopilotTool( + value = "Reports phase", + metadata = { + @CopilotTool.MetadataEntry( + key = "github.com/copilot:safeForTelemetry", + value = @CopilotTool.MetadataValue(flags = { + @CopilotTool.MetadataFlag(name = "name", value = true), + @CopilotTool.MetadataFlag(name = "inputsNames", value = false) + })) + }) +public String reportPhase(@CopilotToolParam("Phase") String phase) { + return phase; +} +``` + +The processor emits this as the `metadata` constructor argument: + +```java +Map.of("github.com/copilot:safeForTelemetry", + Map.of("name", true, "inputsNames", false)) +``` + +For richer values (numbers, arrays, deeper nesting), use the programmatic `ToolDefinition.createWithMetadata(...)` / `ToolDefinition.metadata(...)` API instead. + ### Compile-time validation Because the processor has full access to the source AST, it can emit compile errors for: diff --git a/java/docs/adr/adr-007-native-bundling-strategy.md b/java/docs/adr/adr-007-native-bundling-strategy.md index d2a76d161f..1322e199ef 100644 --- a/java/docs/adr/adr-007-native-bundling-strategy.md +++ b/java/docs/adr/adr-007-native-bundling-strategy.md @@ -1,4 +1,4 @@ -# ADR-007: DRAFT: Native runtime bundling strategy — per-platform classifier JARs +# ADR-007: Native runtime bundling strategy — per-platform classifier JARs ## Context and Problem Statement @@ -133,7 +133,7 @@ The SDK ships a minimal placeholder that detects the current platform at runtime ## Decision Outcome -**Chosen: Option 2 — per-platform classifier JARs.** +**Chosen: Option 2 — per-platform classifier JARs and Option 1 - monolithic jar. Use `maven-assembly-plugin` to allow the creation of the monolithic jar.** ### Rationale @@ -149,6 +149,187 @@ The SDK ships a minimal placeholder that detects the current platform at runtime 6. **Option 3 remains composable.** A download-on-demand fallback can be layered on top of Option 2 for users who prefer it without changing the primary distribution model. The coordination artifact can attempt classpath lookup first, then fall back to a cached download if no matching classifier JAR is present. +7. See section [How can we do Option 2 and Option 1](#how-can-we-do-option-2-and-option-1) for more details. + +## Binding technology: JNA over Panama FFM + +A secondary decision within the scope of this ADR is *how* the coordination artifact calls the C ABI entry points once the correct `runtime.node` binary has been loaded. Two candidates were considered: [JNA](#references) and the [Foreign Function & Memory API](#references) (FFM, the product of [Project Panama](#references), final since Java 22 via [JEP 454](#references)). + +**Chosen: JNA.** FFM was considered and deliberately deferred, for the following reasons: + +1. **Java baseline.** The SDK supports Java 17, where FFM does not exist (it finalized in Java 22). A JNA-based binding is therefore required regardless; adopting FFM today would mean maintaining two parallel binding implementations, not replacing one with the other. + +2. **Consumer-side configuration burden.** FFM downcalls and upcalls are restricted operations under the JDK's integrity-by-default direction ([JEP 472](#references)). An FFM-based SDK would require every consumer to grant native access explicitly — `--enable-native-access=` (or `ALL-UNNAMED` for classpath applications) on the launcher, or an `Enable-Native-Access` manifest attribute. JNA requires no consumer-side configuration today. For an SDK, this flag becomes every downstream application's problem and a predictable source of support issues. (JNA is on the same enforcement trajectory eventually, as it uses JNI internally; this consideration buys time, not immunity.) + +3. **No realizable performance benefit.** FFM's principal advantage over JNA is the elimination of per-call reflective marshalling overhead. The C ABI surface here is a fixed set of ~12 entry points carrying JSON-RPC strings; JSON serialization/deserialization cost dominates the call path, and call frequency is bounded by agent-interaction rates rather than tight loops. The latency difference between JNA and FFM is expected to be unmeasurable in end-to-end SDK usage. This calculus would change only if the transport moved to a high-frequency or shared-memory framing model. + +4. **Upcall lifetime complexity.** The transport is bidirectional: the runtime delivers JSON-RPC responses and server-initiated requests back into Java from native threads. JNA's `Callback` mechanism handles foreign-thread attachment with well-established semantics. FFM upcall stubs require explicit `Arena` lifetime management, where a stub whose arena is closed while the Rust side still holds the function pointer results in a JVM crash. This shifts lifetime reasoning that JNA encapsulates onto the binding layer. + +5. **GraalVM native-image maturity.** JNA's behavior under GraalVM native-image is well established with mature reachability metadata. FFM support in native-image (particularly for upcalls) is newer and varies by GraalVM release. Plausible SDK consumers (e.g., Quarkus/Micronaut-based CLI tools) compile to native images, so this is a compatibility surface the SDK should not destabilize without verification. + +6. **FFM's safety advantages do not apply to this ABI shape.** FFM's `MemorySegment` bounds and lifetime checking pays off when Java code performs structural manipulation of native memory. This surface passes strings through a fixed transport; there is little structural memory work to make safe. + +### Preserving the FFM migration path + +FFM is regarded as the likely eventual binding technology: the JEP 472 endgame applies enforcement pressure to JNA as well, and a ~12-function stable C ABI makes a future migration inexpensive. To keep that path open at low cost: + +- The binding layer is abstracted behind a small internal interface (native load + downcall + upcall registration), so that an FFM implementation can be introduced later — for example, as a multi-release JAR selecting FFM on Java 22+ — without changes to the transport or API layers. +- The decision should be revisited when (a) the SDK's minimum Java baseline moves past 17, or (b) JDK releases begin enforcing `--illegal-native-access=deny` by default, whichever comes first. + +## How can we do Option 2 and Option 1 + +## How it works: classpath resource convention + platform detection + +### 1. Each classifier JAR uses a well-known resource path + +Each per-platform JAR (`copilot-sdk-java-runtime:VERSION:darwin-arm64`, etc.) places its binary under a deterministic path inside the JAR: + +``` +native/darwin-arm64/runtime.node +native/darwin-arm64/platform.properties +``` + +When `maven-assembly-plugin` creates the uber-jar, it unpacks all dependencies and merges them. The resulting uber-jar contains: + +``` +com/github/copilot/sdk/... (Java classes) +native/linux-x64/runtime.node +native/linux-arm64/runtime.node +native/linuxmusl-x64/runtime.node +native/linuxmusl-arm64/runtime.node +native/darwin-x64/runtime.node +native/darwin-arm64/runtime.node +native/win32-x64/runtime.node +native/win32-arm64/runtime.node +``` + +### 2. The coordination artifact selects at runtime via `getResourceAsStream` + +```java +public class NativeRuntimeLoader { + + public Path loadRuntime() { + String classifier = detectPlatformClassifier(); + String resourcePath = "native/" + classifier + "/runtime.node"; + + try (InputStream in = getClass().getClassLoader() + .getResourceAsStream(resourcePath)) { + if (in == null) { + throw new UnsupportedOperationException( + "No native runtime for platform: " + classifier); + } + Path cached = getCachePath(classifier); + if (!Files.exists(cached)) { + Files.createDirectories(cached.getParent()); + Files.copy(in, cached); + // Make executable on Unix + cached.toFile().setExecutable(true); + } + return cached; + } + } + + private String detectPlatformClassifier() { + String os = normalizeOs(System.getProperty("os.name")); + String arch = normalizeArch(System.getProperty("os.arch")); + String libc = "linux".equals(os) ? detectLinuxLibc() : ""; + + // Produces: "linux-x64", "linuxmusl-arm64", "darwin-arm64", "win32-x64", etc. + return (libc.isEmpty() ? os : os + libc) + "-" + arch; + } + + private String detectLinuxLibc() { + // Read ELF PT_INTERP from /proc/self/exe + // If interpreter contains "/ld-musl-" → "musl" + // Otherwise → "" (glibc is the default/unmarked case for "linux-") + // ... + } + + private Path getCachePath(String classifier) { + String version = getClass().getPackage().getImplementationVersion(); + return Path.of(System.getProperty("user.home"), + ".copilot", "runtime-cache", version, classifier, "runtime.node"); + } +} +``` + +### 3. JNA loads from the extracted path + +Once extracted to a known filesystem path, JNA loads it directly: + +```java +NativeLibrary lib = NativeLibrary.getInstance(extractedPath.toString()); +// Or via a mapped interface: +CopilotRuntime runtime = Native.load(extractedPath.toString(), CopilotRuntime.class); +``` + +### Key insight: the same code works in both modes + +The beauty is that `getResourceAsStream("native/darwin-arm64/runtime.node")` works identically whether: + +- The native lives in a **separate classifier JAR** on the classpath (normal dev dependency), OR +- It's been **merged into an uber-jar** by `maven-assembly-plugin` + +The classloader doesn't care which JAR file the resource came from — it searches the entire classpath. This means **zero code changes** between the two consumption models. + +--- + +## Assembly plugin configuration (consumer-side) + +A consumer building a portable uber-jar would configure: + +```xml + + maven-assembly-plugin + + + jar-with-dependencies + + + +``` + +With all classifier JARs declared as dependencies: + +```xml + + + com.github + copilot-sdk-java + ${copilot.version} + + + + com.github + copilot-sdk-java-runtime + ${copilot.version} + linux-x64 + + + com.github + copilot-sdk-java-runtime + ${copilot.version} + darwin-arm64 + + + +``` + +--- + +## Why this works cleanly + +| Concern | How it's handled | +|---------|-----------------| +| No resource path collisions | Each platform has its own subdirectory (`native//`) | +| Extraction only happens once | Cached to `~/.copilot/runtime-cache///` | +| Works without uber-jar too | Same `getResourceAsStream` call — classloader finds it in the separate JAR | +| Subset selection | Consumer declares only the classifiers they need; missing platforms get a clear error at runtime | +| JNA loading | `NativeLibrary.getInstance(path)` loads from an absolute filesystem path after extraction — no JNA platform-detection magic needed | + +The pattern is identical to how DJL's `LibUtils.loadLibrary()` works — detect platform, construct resource path, extract if needed, load via absolute path. + + ## Consequences - A new Maven module (`copilot-sdk-java-runtime` or similar) is introduced to hold the per-platform native JARs. The existing `copilot-sdk-java` coordination artifact depends on it. @@ -156,7 +337,7 @@ The SDK ships a minimal placeholder that detects the current platform at runtime 1. Detects OS, architecture, and Linux libc variant deterministically as described above. 2. Locates the matching `runtime.node` binary on the classpath (via `getResourceAsStream` from the classifier JAR). 3. Extracts the binary to a temporary or cached location (e.g., `~/.copilot/runtime-cache/`) if not already present. - 4. Loads it via [JNA](#references) using the C ABI entry points. + 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. - The release pipeline for `github/copilot-agent-runtime` must produce the per-platform `runtime.node` binaries as inputs to the Java SDK publish workflow. The per-platform `pkg-tarballs-` artifacts from the `publish-cli.yml` workflow are the authoritative source. - Each release of `copilot-sdk-java` publishes 6 (or 8) classifier JARs to Maven Central alongside the coordination JAR. - The version of the bundled `runtime.node` is recorded in the coordination JAR's manifest and queryable at runtime, enabling diagnostics and mismatch detection. @@ -183,6 +364,10 @@ The SDK ships a minimal placeholder that detects the current platform at runtime | **glibc** (GNU C Library) | The standard C runtime library on most mainstream Linux distributions (Debian, Ubuntu, RHEL, Fedora, SLES). Binaries linked against glibc require the same version or newer to be present at runtime. The `runtime.node` glibc build requires glibc ≥ 2.28. | https://www.gnu.org/software/libc/ | | **musl libc** | An alternative C standard library optimised for static linking and used as the default libc on Alpine Linux. Not binary-compatible with glibc; a separate `runtime.node` build is required. | https://musl.libc.org/ | | **MSVC CRT** (Microsoft Visual C++ Runtime) | The C runtime library shipped with Visual Studio. When compiled with `+crt-static` (as `runtime.node` is on Windows), it is statically linked into the binary and the end-user does not need to install the Visual C++ Redistributable. | https://learn.microsoft.com/en-us/cpp/c-runtime-library/c-run-time-library-reference | +| **Project Panama** | The OpenJDK project that produced the Foreign Function & Memory API as the modern, supported replacement for JNI-based native interop. | https://openjdk.org/projects/panama/ | +| **FFM** (Foreign Function & Memory API) | The `java.lang.foreign` API for calling native functions and managing native memory from Java, finalized in Java 22. Considered and deferred as the binding technology for this SDK; see [Binding technology](#binding-technology-jna-over-panama-ffm). | https://docs.oracle.com/en/java/javase/22/core/foreign-function-and-memory-api.html | +| **JEP 454** | The JDK Enhancement Proposal that finalized the FFM API in Java 22. | https://openjdk.org/jeps/454 | +| **JEP 472** | "Prepare to Restrict the Use of JNI" — part of the JDK's integrity-by-default direction under which native access (via JNI or FFM) requires explicit consumer opt-in (`--enable-native-access`). Drives both the FFM configuration-burden concern and the expectation that JNA itself will eventually require the same opt-in. | https://openjdk.org/jeps/472 | | **DJL** (Deep Java Library) | Amazon's open-source Java framework for ML inference, used here as a reference for the per-platform classifier JAR distribution pattern. Its PyTorch native artifacts (`pytorch-native-cpu-*-.jar`) are the direct model for the proposed `copilot-sdk-java-runtime:VERSION:` artifacts. | https://djl.ai/ | | **os-maven-plugin** | A Maven extension that detects the current OS and architecture and exposes them as properties (e.g., `${os.detected.classifier}`) so that `` values can be resolved at build time rather than hardcoded. | https://github.com/trustin/os-maven-plugin | | **ONNX Runtime** | Microsoft's cross-platform ML inference runtime, used in this ADR as the size comparable for a monolithic all-platform JAR (~130 MB, Option 1). | https://onnxruntime.ai/ | diff --git a/java/jbang-example.java b/java/jbang-example.java index 9bb83457ad..db49adf4cd 100644 --- a/java/jbang-example.java +++ b/java/jbang-example.java @@ -1,5 +1,5 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS com.github:copilot-sdk-java:1.0.6-preview.1-01 +//DEPS com.github:copilot-sdk-java:1.0.7-01 import com.github.copilot.CopilotClient; import com.github.copilot.generated.AssistantMessageEvent; import com.github.copilot.generated.SessionUsageInfoEvent; diff --git a/java/pom.xml b/java/pom.xml index 30e9644bba..ac95090b8b 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java - 1.0.7-SNAPSHOT + 1.0.8-SNAPSHOT jar GitHub Copilot SDK :: Java @@ -86,7 +86,7 @@ DO NOT EDIT MANUALLY. Updated by the update-copilot-dependency workflow. --> - ^1.0.69-2 + ^1.0.71 diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts index 7c2a5cebea..1e5ff1d543 100644 --- a/java/scripts/codegen/java.ts +++ b/java/scripts/codegen/java.ts @@ -21,6 +21,12 @@ const REPO_ROOT = path.resolve(__dirname, "../.."); /** Event types to exclude from generation (internal/legacy types) */ const EXCLUDED_EVENT_TYPES = new Set(["session.import_legacy"]); +function isSchemaInternal(schema: JSONSchema7 | null | undefined): boolean { + return typeof schema === "object" && + schema !== null && + (schema as Record).visibility === "internal"; +} + const AUTO_GENERATED_HEADER = `// AUTO-GENERATED FILE - DO NOT EDIT`; const GENERATED_FROM_SESSION_EVENTS = `// Generated from: session-events.schema.json`; const GENERATED_FROM_API = `// Generated from: api.schema.json`; @@ -725,7 +731,7 @@ function extractEventVariants(schema: JSONSchema7): EventVariant[] { deprecated: (variant as unknown as Record).deprecated === true, }; }) - .filter((v) => !EXCLUDED_EVENT_TYPES.has(v.typeName)); + .filter((v) => !EXCLUDED_EVENT_TYPES.has(v.typeName) && !isSchemaInternal(v.dataSchema)); } async function generateSessionEvents(schemaPath: string): Promise { diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json index 7b026906c7..372e3daab5 100644 --- a/java/scripts/codegen/package-lock.json +++ b/java/scripts/codegen/package-lock.json @@ -6,9 +6,9 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.69-2", + "@github/copilot": "^1.0.71", "json-schema": "^0.4.0", - "tsx": "^4.22.4" + "tsx": "^4.23.1" } }, "node_modules/@esbuild/aix-ppc64": { @@ -428,9 +428,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.69-2.tgz", - "integrity": "sha512-D/fvtb8RZVL0jbDjU5k7M2J08mr2eB0n7+NwUAJw/9+s1lmZBN6F3OqKh83Uo03d8oLW32ITJhqoxvs85pyiLw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.71.tgz", + "integrity": "sha512-F3axBi+sXSLYDJbxCBW36bM6MYKNC2rlyAf3Ivo/MjiHKKJW7j5AmaR1IRYS9Gt8r9mxOwWFM1cJFA+CuLaR8g==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -439,20 +439,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.69-2", - "@github/copilot-darwin-x64": "1.0.69-2", - "@github/copilot-linux-arm64": "1.0.69-2", - "@github/copilot-linux-x64": "1.0.69-2", - "@github/copilot-linuxmusl-arm64": "1.0.69-2", - "@github/copilot-linuxmusl-x64": "1.0.69-2", - "@github/copilot-win32-arm64": "1.0.69-2", - "@github/copilot-win32-x64": "1.0.69-2" + "@github/copilot-darwin-arm64": "1.0.71", + "@github/copilot-darwin-x64": "1.0.71", + "@github/copilot-linux-arm64": "1.0.71", + "@github/copilot-linux-x64": "1.0.71", + "@github/copilot-linuxmusl-arm64": "1.0.71", + "@github/copilot-linuxmusl-x64": "1.0.71", + "@github/copilot-win32-arm64": "1.0.71", + "@github/copilot-win32-x64": "1.0.71" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.69-2.tgz", - "integrity": "sha512-643mEEP/0FfZQvxNmg9UquVJv01gJ2QTdi2qabT/cPX2jLpgrPRjRvikX/XewAzGSUSzy4pyx5qyDYIr7TjUcw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.71.tgz", + "integrity": "sha512-mEWzyqbqRAWgyU7i2uuSRoVPx/TwaFQX0nZmw0bc30aJ0BnO7cy2kYQyCHw8ykmf/tfxT0xauZ6k0BOFmWizzQ==", "cpu": [ "arm64" ], @@ -466,9 +466,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.69-2.tgz", - "integrity": "sha512-Lx4+8lLJdI8bhA/pLWcRMuae3nxM7SivhWpS5lWsQyPrsdF5g1vYPtmo7ip3hc65jOxpzVWImc9FgQ8d5fKvOA==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.71.tgz", + "integrity": "sha512-Md9yEg406OBVBx3w4PeEj62TubulVLBcHleqmCoOoUmPgUxPZotUbrqz3rtbzADbXfrrD7JWvVsbd2UiNL194w==", "cpu": [ "x64" ], @@ -482,9 +482,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.69-2.tgz", - "integrity": "sha512-IheFTABphOz4QIqTfN0sLwF57yZtOm0IWJbgUtQe9WkiduD9L8Ii1EtJKbpVPygIgwX4LbcYTCyCMglZjLPliw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.71.tgz", + "integrity": "sha512-ykLJYOqBj3jRB5IJCDugLClAqbr7DmtTbUjlNY7+Jdq/n6i+d7xUQGclf1IWL5gnxbGQVAf+zkToD+sRM389Kg==", "cpu": [ "arm64" ], @@ -498,9 +498,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.69-2.tgz", - "integrity": "sha512-FHMSL5sUqH55U2kyaRaUGNGguyr5SI2ce5FriyPnVQWvYUSuRbdfEo9mC8jeONecD0sO0FQuWwbMk2/JkQOHTA==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.71.tgz", + "integrity": "sha512-pC0FNHG+BBwZd6yZlM85kkAGN+uJhM6o+THi76N2GnnSxmw7+remb1mvYxdgRVbdCm+LBUIbCKRWJLuMwrfb6A==", "cpu": [ "x64" ], @@ -514,9 +514,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.69-2.tgz", - "integrity": "sha512-c8yvsxEUNjwaQOU8zO0VFg7LWqTDDvx2SV3rzNz4FSCxTZf0SmcZw6TczsTauKO/0hAq5lfLH4d1pJkTLQ62Zw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.71.tgz", + "integrity": "sha512-hBmDljFTjacxqZTasCEy43H8EIzuXB/hHEBBCMFjhB9J00nIxsO6Dh0woTifKpx7knTYZdpTjjca3D0pAoZlUA==", "cpu": [ "arm64" ], @@ -530,9 +530,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.69-2.tgz", - "integrity": "sha512-9OQVqN3muGyBePmDY7jGsfhGfCAqvauSZqoWZfX+n28YtZrXTXR7IfGSuzBwZq/isOhopaIplmOc19ZYIMeCEw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.71.tgz", + "integrity": "sha512-CfTXU8pa5dxRz22xQzoi3TiG1PJo9+WR8PRDiPSdkIBSyPJ1NvX87DJmfXjTgeAfR+wkjt/p0keDCaBBVhNmUA==", "cpu": [ "x64" ], @@ -546,9 +546,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.69-2.tgz", - "integrity": "sha512-eEKJid8T+tI70dn+2UL4s2b2AQcoHaAmaJ6x/7qTksdVuaII27vuVO/yE4wpWkhYl+yvI3Ml/nkavgrms7Y/Pw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.71.tgz", + "integrity": "sha512-+HI1DokixXhHUahj06Fw67ZAigBuXKC58BFma4UJOGrQsDgwOSbqeTQHCw6vuymzjKlg3sactfsCUTaefkjscQ==", "cpu": [ "arm64" ], @@ -562,9 +562,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.69-2.tgz", - "integrity": "sha512-usFpfQudpOEft/OYQaJ+c0MJOgP9bXZ1vctx5mRgC6d8+0dIRbLCZ5rVuir1K7zyS246uYHZgO/t0sMQH7pOdQ==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.71.tgz", + "integrity": "sha512-02kXOBd9CwBbCaztuf71WYWn+uGapCuiaasomN4tcMH3HBVZ4gi3J0ZUoRcgcS80xh81uQyeBHbnUKzb/RE/9A==", "cpu": [ "x64" ], @@ -648,9 +648,9 @@ "license": "(AFL-2.1 OR BSD-3-Clause)" }, "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.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", "license": "MIT", "dependencies": { "esbuild": "~0.28.0" diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json index b49ecba30c..32c609be41 100644 --- a/java/scripts/codegen/package.json +++ b/java/scripts/codegen/package.json @@ -7,8 +7,8 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.69-2", + "@github/copilot": "^1.0.71", "json-schema": "^0.4.0", - "tsx": "^4.22.4" + "tsx": "^4.23.1" } } diff --git a/java/src/generated/java/com/github/copilot/generated/AssistantMessageEvent.java b/java/src/generated/java/com/github/copilot/generated/AssistantMessageEvent.java index 4fce0133e9..fdbdc0afd7 100644 --- a/java/src/generated/java/com/github/copilot/generated/AssistantMessageEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/AssistantMessageEvent.java @@ -59,6 +59,8 @@ public record AssistantMessageEventData( @JsonProperty("interactionId") String interactionId, /** GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs */ @JsonProperty("requestId") String requestId, + /** Client-minted request id (x-request-id header) echoed by the server. Distinct from requestId (x-github-request-id) and serviceRequestId (x-copilot-service-request-id). */ + @JsonProperty("clientRequestId") String clientRequestId, /** Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation */ @JsonProperty("serviceRequestId") String serviceRequestId, /** Provider's completion / response identifier; shared across all chunks of a single API call. Used to group multi-chunk assistant utterances. */ diff --git a/java/src/generated/java/com/github/copilot/generated/AssistantServerToolProgressEvent.java b/java/src/generated/java/com/github/copilot/generated/AssistantServerToolProgressEvent.java new file mode 100644 index 0000000000..462a573b3e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/AssistantServerToolProgressEvent.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 "assistant.server_tool_progress". Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantServerToolProgressEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.server_tool_progress"; } + + @JsonProperty("data") + private AssistantServerToolProgressEventData data; + + public AssistantServerToolProgressEventData getData() { return data; } + public void setData(AssistantServerToolProgressEventData data) { this.data = data; } + + /** Data payload for {@link AssistantServerToolProgressEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantServerToolProgressEventData( + /** Position of the hosted tool call in the response output. Stable across the call's lifecycle events (unlike the provider's per-event item id, which CAPI rotates), so the host keys the live in-progress row on it. */ + @JsonProperty("outputIndex") Long outputIndex, + /** Kind of hosted server tool that is running. Only `web_search` is emitted today. */ + @JsonProperty("kind") String kind, + /** Lifecycle status of the hosted call: `in_progress`, `searching`, or `completed`. */ + @JsonProperty("status") String status + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/AssistantToolCallDeltaEvent.java b/java/src/generated/java/com/github/copilot/generated/AssistantToolCallDeltaEvent.java new file mode 100644 index 0000000000..72b629c0c6 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/AssistantToolCallDeltaEvent.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * 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.tool_call_delta". Streaming tool-call input delta for incremental tool-call updates + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantToolCallDeltaEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.tool_call_delta"; } + + @JsonProperty("data") + private AssistantToolCallDeltaEventData data; + + public AssistantToolCallDeltaEventData getData() { return data; } + public void setData(AssistantToolCallDeltaEventData data) { this.data = data; } + + /** Data payload for {@link AssistantToolCallDeltaEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantToolCallDeltaEventData( + /** Tool call ID this delta belongs to, matching the corresponding assistant.message tool request */ + @JsonProperty("toolCallId") String toolCallId, + /** Name of the tool being invoked, when known from the stream */ + @JsonProperty("toolName") String toolName, + /** Tool call type, when known from the stream */ + @JsonProperty("toolType") AssistantMessageToolRequestType toolType, + /** Raw provider tool input fragment to append for this tool call. Function/tool-use providers stream serialized JSON argument text (so newlines inside JSON string values may appear as escaped `\n` until the accumulated JSON is parsed); custom tool calls stream raw custom input. */ + @JsonProperty("inputDelta") String inputDelta + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/AutoModeResolvedReasoningBucket.java b/java/src/generated/java/com/github/copilot/generated/AutoModeResolvedReasoningBucket.java new file mode 100644 index 0000000000..3034b0bf16 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/AutoModeResolvedReasoningBucket.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; + +/** + * Coarse request-difficulty bucket for UX explainability + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AutoModeResolvedReasoningBucket { + /** The {@code low} variant. */ + LOW("low"), + /** The {@code medium} variant. */ + MEDIUM("medium"), + /** The {@code high} variant. */ + HIGH("high"); + + private final String value; + AutoModeResolvedReasoningBucket(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AutoModeResolvedReasoningBucket fromValue(String value) { + for (AutoModeResolvedReasoningBucket v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AutoModeResolvedReasoningBucket value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/CanvasRegistryChangedCanvas.java b/java/src/generated/java/com/github/copilot/generated/CanvasRegistryChangedCanvas.java index dbafc5d626..12518491e8 100644 --- a/java/src/generated/java/com/github/copilot/generated/CanvasRegistryChangedCanvas.java +++ b/java/src/generated/java/com/github/copilot/generated/CanvasRegistryChangedCanvas.java @@ -32,6 +32,8 @@ public record CanvasRegistryChangedCanvas( @JsonProperty("displayName") String displayName, /** Short, single-sentence description shown to the agent in canvas catalogs. */ @JsonProperty("description") String description, + /** Host-local PNG path for the canvas icon, when supplied */ + @JsonProperty("icon") String icon, /** JSON Schema for canvas open input */ @JsonProperty("inputSchema") Object inputSchema, /** Actions the agent or host may invoke */ diff --git a/java/src/generated/java/com/github/copilot/generated/HeaderEntry.java b/java/src/generated/java/com/github/copilot/generated/HeaderEntry.java new file mode 100644 index 0000000000..14828d32e3 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/HeaderEntry.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; + +/** + * Single HTTP header entry as a name/value pair. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record HeaderEntry( + /** HTTP response header name as observed by the runtime. */ + @JsonProperty("name") String name, + /** HTTP response header value as observed by the runtime. */ + @JsonProperty("value") String value +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/ManagedSettingsResolvedSource.java b/java/src/generated/java/com/github/copilot/generated/ManagedSettingsResolvedSource.java new file mode 100644 index 0000000000..1ffa100635 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/ManagedSettingsResolvedSource.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; + +/** + * Which channel supplied the effective enterprise managed settings (highest-authority present layer wins wholesale) + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ManagedSettingsResolvedSource { + /** The {@code server} variant. */ + SERVER("server"), + /** The {@code device} variant. */ + DEVICE("device"), + /** The {@code none} variant. */ + NONE("none"); + + private final String value; + ManagedSettingsResolvedSource(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ManagedSettingsResolvedSource fromValue(String value) { + for (ManagedSettingsResolvedSource v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ManagedSettingsResolvedSource value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthHttpResponse.java b/java/src/generated/java/com/github/copilot/generated/McpOauthHttpResponse.java new file mode 100644 index 0000000000..bed8e0ac62 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/McpOauthHttpResponse.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * 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 java.util.List; +import javax.annotation.processing.Generated; + +/** + * Raw HTTP response details from the OAuth auth challenge, as observed by the runtime. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpOauthHttpResponse( + /** HTTP status code returned with the auth challenge. */ + @JsonProperty("statusCode") Long statusCode, + /** HTTP response headers as observed by the runtime. Order and casing are transport-dependent, and duplicate header names may appear multiple times. */ + @JsonProperty("headers") List headers, + /** Complete UTF-8 response body for host-specific challenge handling, including an empty string for an empty body. Omitted when the complete body is not valid UTF-8; body read failures fail the HTTP operation rather than exposing a partial response. */ + @JsonProperty("body") String body +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java index 67413d382f..f21f84cd4b 100644 --- a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java @@ -44,6 +44,8 @@ public record McpOauthRequiredEventData( @JsonProperty("staticClientConfig") McpOauthRequiredStaticClientConfig staticClientConfig, /** OAuth WWW-Authenticate parameters parsed from the auth challenge, if available */ @JsonProperty("wwwAuthenticateParams") McpOauthWWWAuthenticateParams wwwAuthenticateParams, + /** Raw HTTP response details from the OAuth auth challenge, as observed by the runtime. Header order and casing are transport-dependent, and duplicate header names may appear multiple times. */ + @JsonProperty("httpResponse") McpOauthHttpResponse httpResponse, /** Raw OAuth protected-resource metadata document fetched for the MCP server, if available */ @JsonProperty("resourceMetadata") String resourceMetadata, /** Why the runtime is requesting host-provided OAuth credentials. */ diff --git a/java/src/generated/java/com/github/copilot/generated/McpPromptsListChangedEvent.java b/java/src/generated/java/com/github/copilot/generated/McpPromptsListChangedEvent.java new file mode 100644 index 0000000000..805328d3c1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/McpPromptsListChangedEvent.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 "mcp.prompts.list_changed". Payload identifying the MCP server associated with a list change. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpPromptsListChangedEvent extends SessionEvent { + + @Override + public String getType() { return "mcp.prompts.list_changed"; } + + @JsonProperty("data") + private McpPromptsListChangedEventData data; + + public McpPromptsListChangedEventData getData() { return data; } + public void setData(McpPromptsListChangedEventData data) { this.data = data; } + + /** Data payload for {@link McpPromptsListChangedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record McpPromptsListChangedEventData( + /** Name of the MCP server whose list changed */ + @JsonProperty("serverName") String serverName + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/McpResourcesListChangedEvent.java b/java/src/generated/java/com/github/copilot/generated/McpResourcesListChangedEvent.java new file mode 100644 index 0000000000..f1a613b6fa --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/McpResourcesListChangedEvent.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 "mcp.resources.list_changed". Payload identifying the MCP server associated with a list change. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpResourcesListChangedEvent extends SessionEvent { + + @Override + public String getType() { return "mcp.resources.list_changed"; } + + @JsonProperty("data") + private McpResourcesListChangedEventData data; + + public McpResourcesListChangedEventData getData() { return data; } + public void setData(McpResourcesListChangedEventData data) { this.data = data; } + + /** Data payload for {@link McpResourcesListChangedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record McpResourcesListChangedEventData( + /** Name of the MCP server whose list changed */ + @JsonProperty("serverName") String serverName + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/McpToolsListChangedEvent.java b/java/src/generated/java/com/github/copilot/generated/McpToolsListChangedEvent.java new file mode 100644 index 0000000000..4255b8544f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/McpToolsListChangedEvent.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 "mcp.tools.list_changed". Payload identifying the MCP server associated with a list change. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpToolsListChangedEvent extends SessionEvent { + + @Override + public String getType() { return "mcp.tools.list_changed"; } + + @JsonProperty("data") + private McpToolsListChangedEventData data; + + public McpToolsListChangedEventData getData() { return data; } + public void setData(McpToolsListChangedEventData data) { this.data = data; } + + /** Data payload for {@link McpToolsListChangedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record McpToolsListChangedEventData( + /** Name of the MCP server whose list changed */ + @JsonProperty("serverName") String serverName + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/SessionAutoModeResolvedEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionAutoModeResolvedEvent.java new file mode 100644 index 0000000000..f79be388ec --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/SessionAutoModeResolvedEvent.java @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * 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 java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Session event "session.auto_mode_resolved". Auto Intent resolution: the concrete model the session settled on for the first prompt of an auto-mode session, and why. Lets SDK clients render the chosen model and the full reason it was picked. The core selection fields (chosenModel/reasoningBucket/categoryScores) are stable; the routing-analytics fields (predictedLabel/confidence/candidateModels) mirror the upstream intent service and may evolve, hence the event's experimental stability. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionAutoModeResolvedEvent extends SessionEvent { + + @Override + public String getType() { return "session.auto_mode_resolved"; } + + @JsonProperty("data") + private SessionAutoModeResolvedEventData data; + + public SessionAutoModeResolvedEventData getData() { return data; } + public void setData(SessionAutoModeResolvedEventData data) { this.data = data; } + + /** Data payload for {@link SessionAutoModeResolvedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionAutoModeResolvedEventData( + /** The concrete model the session will use after any intent refinement */ + @JsonProperty("chosenModel") String chosenModel, + /** Coarse request-difficulty bucket, for explaining why a model was chosen ("picked X because this looks like high-reasoning work") */ + @JsonProperty("reasoningBucket") AutoModeResolvedReasoningBucket reasoningBucket, + /** Per-category classifier scores (0-1) behind the bucket: the granular HYDRA capability scores (reasoning, code_gen, debugging, tool_use), or the binary needs_reasoning/no_reasoning scores when HYDRA didn't run. Lets clients show a breakdown rather than just the bucket. */ + @JsonProperty("categoryScores") Map categoryScores, + /** The predicted classifier label (e.g. `needs_reasoning`), when available */ + @JsonProperty("predictedLabel") String predictedLabel, + /** Classifier confidence for the predicted label, when available */ + @JsonProperty("confidence") Double confidence, + /** Ordered candidate model list the router returned, when not a fallback */ + @JsonProperty("candidateModels") List candidateModels + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/SessionCanvasOpenedEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionCanvasOpenedEvent.java index 975737b2f8..018e6a234d 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionCanvasOpenedEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionCanvasOpenedEvent.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * Session event "session.canvas.opened". Payload of `session.canvas.opened` with canvas instance and provider IDs plus optional title, status, URL, and input. + * Session event "session.canvas.opened". Payload of `session.canvas.opened` with canvas instance and provider IDs plus optional icon, title, status, URL, and input. * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) @@ -42,6 +42,8 @@ public record SessionCanvasOpenedEventData( @JsonProperty("extensionName") String extensionName, /** Provider-local canvas identifier */ @JsonProperty("canvasId") String canvasId, + /** Host-local PNG path for the canvas icon, when supplied */ + @JsonProperty("icon") String icon, /** Rendered title */ @JsonProperty("title") String title, /** Provider-supplied status text */ diff --git a/java/src/generated/java/com/github/copilot/generated/SessionEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionEvent.java index 7bf0660f6c..d15da0c41c 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionEvent.java @@ -58,8 +58,10 @@ @JsonSubTypes.Type(value = PendingMessagesModifiedEvent.class, name = "pending_messages.modified"), @JsonSubTypes.Type(value = AssistantTurnStartEvent.class, name = "assistant.turn_start"), @JsonSubTypes.Type(value = AssistantIntentEvent.class, name = "assistant.intent"), + @JsonSubTypes.Type(value = AssistantServerToolProgressEvent.class, name = "assistant.server_tool_progress"), @JsonSubTypes.Type(value = AssistantReasoningEvent.class, name = "assistant.reasoning"), @JsonSubTypes.Type(value = AssistantReasoningDeltaEvent.class, name = "assistant.reasoning_delta"), + @JsonSubTypes.Type(value = AssistantToolCallDeltaEvent.class, name = "assistant.tool_call_delta"), @JsonSubTypes.Type(value = AssistantStreamingDeltaEvent.class, name = "assistant.streaming_delta"), @JsonSubTypes.Type(value = AssistantMessageEvent.class, name = "assistant.message"), @JsonSubTypes.Type(value = AssistantMessageStartEvent.class, name = "assistant.message_start"), @@ -108,6 +110,8 @@ @JsonSubTypes.Type(value = AutoModeSwitchCompletedEvent.class, name = "auto_mode_switch.completed"), @JsonSubTypes.Type(value = SessionLimitsExhaustedRequestedEvent.class, name = "session_limits_exhausted.requested"), @JsonSubTypes.Type(value = SessionLimitsExhaustedCompletedEvent.class, name = "session_limits_exhausted.completed"), + @JsonSubTypes.Type(value = SessionAutoModeResolvedEvent.class, name = "session.auto_mode_resolved"), + @JsonSubTypes.Type(value = SessionManagedSettingsResolvedEvent.class, name = "session.managed_settings_resolved"), @JsonSubTypes.Type(value = CommandsChangedEvent.class, name = "commands.changed"), @JsonSubTypes.Type(value = CapabilitiesChangedEvent.class, name = "capabilities.changed"), @JsonSubTypes.Type(value = ExitPlanModeRequestedEvent.class, name = "exit_plan_mode.requested"), @@ -118,6 +122,9 @@ @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 = 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"), @JsonSubTypes.Type(value = SessionExtensionsLoadedEvent.class, name = "session.extensions_loaded"), @JsonSubTypes.Type(value = SessionCanvasOpenedEvent.class, name = "session.canvas.opened"), @JsonSubTypes.Type(value = SessionCanvasRegistryChangedEvent.class, name = "session.canvas.registry_changed"), @@ -163,8 +170,10 @@ public abstract sealed class SessionEvent permits PendingMessagesModifiedEvent, AssistantTurnStartEvent, AssistantIntentEvent, + AssistantServerToolProgressEvent, AssistantReasoningEvent, AssistantReasoningDeltaEvent, + AssistantToolCallDeltaEvent, AssistantStreamingDeltaEvent, AssistantMessageEvent, AssistantMessageStartEvent, @@ -213,6 +222,8 @@ public abstract sealed class SessionEvent permits AutoModeSwitchCompletedEvent, SessionLimitsExhaustedRequestedEvent, SessionLimitsExhaustedCompletedEvent, + SessionAutoModeResolvedEvent, + SessionManagedSettingsResolvedEvent, CommandsChangedEvent, CapabilitiesChangedEvent, ExitPlanModeRequestedEvent, @@ -223,6 +234,9 @@ public abstract sealed class SessionEvent permits SessionCustomAgentsUpdatedEvent, SessionMcpServersLoadedEvent, SessionMcpServerStatusChangedEvent, + McpToolsListChangedEvent, + McpResourcesListChangedEvent, + McpPromptsListChangedEvent, SessionExtensionsLoadedEvent, SessionCanvasOpenedEvent, SessionCanvasRegistryChangedEvent, diff --git a/java/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java new file mode 100644 index 0000000000..7cc495269f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * 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 java.util.List; +import javax.annotation.processing.Generated; + +/** + * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and where they came from, so SDK clients can show users what is enterprise-managed and by which authority. 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; for a session-independent pull, use the SDK `getManagedSettings()` API, which returns the identical payload. Managed settings have a single authoritative source, so the highest-authority present layer (server > device) wins wholesale; `bypassPermissionsDisabled` is deny-wins across layers. Marked experimental while the managed-settings surface stabilizes. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionManagedSettingsResolvedEvent extends SessionEvent { + + @Override + public String getType() { return "session.managed_settings_resolved"; } + + @JsonProperty("data") + private SessionManagedSettingsResolvedEventData data; + + public SessionManagedSettingsResolvedEventData getData() { return data; } + public void setData(SessionManagedSettingsResolvedEventData data) { this.data = data; } + + /** Data payload for {@link SessionManagedSettingsResolvedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionManagedSettingsResolvedEventData( + /** Which channel supplied the effective managed settings (the winning layer), or `none` when no policy is in force */ + @JsonProperty("source") ManagedSettingsResolvedSource source, + /** Whether the server (account/org) managed-settings layer was present */ + @JsonProperty("serverManaged") Boolean serverManaged, + /** Whether the device (MDM/plist/registry/file) managed-settings layer was present */ + @JsonProperty("deviceManaged") Boolean deviceManaged, + /** 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 enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true. */ + @JsonProperty("bypassPermissionsDisabled") Boolean bypassPermissionsDisabled, + /** The setting keys under enterprise management in the effective managed settings (e.g. `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force. */ + @JsonProperty("managedKeys") List managedKeys, + /** The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. */ + @JsonProperty("settings") Object settings + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java index da0279757d..f12b86d088 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java @@ -46,6 +46,10 @@ public record SessionModelChangeEventData( @JsonProperty("previousReasoningSummary") ReasoningSummary previousReasoningSummary, /** Reasoning summary mode after the model change, if applicable */ @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, + /** Output verbosity level before the model change, if applicable */ + @JsonProperty("previousVerbosity") Verbosity previousVerbosity, + /** Output verbosity level after the model change, if applicable */ + @JsonProperty("verbosity") Verbosity verbosity, /** Context tier after the model change; null explicitly clears a previously selected tier */ @JsonProperty("contextTier") ContextTier contextTier, /** Reason the change happened, when not user-initiated. Currently `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path. UI clients can use this to render contextual copy. */ diff --git a/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java index b70e24ed34..6efac46f27 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java @@ -47,6 +47,8 @@ public record SessionResumeEventData( @JsonProperty("reasoningEffort") String reasoningEffort, /** Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") */ @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, + /** Output verbosity level used for model calls, if applicable (e.g. "low", "medium", "high") */ + @JsonProperty("verbosity") Verbosity verbosity, /** Context tier currently selected at resume time; null when no tier is active */ @JsonProperty("contextTier") ContextTier contextTier, /** Session limits currently configured at resume time; null when no limits are active */ diff --git a/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java index fcd1928874..4beb487c31 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java @@ -51,6 +51,8 @@ public record SessionStartEventData( @JsonProperty("reasoningEffort") String reasoningEffort, /** Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") */ @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, + /** Output verbosity level used for model calls, if applicable (e.g. "low", "medium", "high") */ + @JsonProperty("verbosity") Verbosity verbosity, /** Context tier selected at session creation time for models with tiered context pricing; null when no tier is selected (e.g., non-tiered model) */ @JsonProperty("contextTier") ContextTier contextTier, /** Session limits configured at session creation time, if any */ diff --git a/java/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteEvent.java b/java/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteEvent.java index b4e82d2685..99d138b1e6 100644 --- a/java/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteEvent.java @@ -41,6 +41,8 @@ public record ToolExecutionCompleteEventData( @JsonProperty("success") Boolean success, /** Model identifier that generated this tool call */ @JsonProperty("model") String model, + /** FIDES IFC label projected from tool ingress metadata (MCP `CallToolResult._meta` or synthesized built-in ingress labels). Persisted as `{ ifc: ... }` so the label survives session resume, including model-visible failure results. Experimental. */ + @JsonProperty("mcpMeta") Object mcpMeta, /** CAPI interaction ID for correlating this tool execution with upstream telemetry */ @JsonProperty("interactionId") String interactionId, /** Whether this tool call was explicitly requested by the user rather than the assistant */ diff --git a/java/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteResult.java b/java/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteResult.java index 7459d5517f..f7f08d93c6 100644 --- a/java/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteResult.java +++ b/java/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteResult.java @@ -35,6 +35,8 @@ public record ToolExecutionCompleteResult( /** Structured content (arbitrary JSON) returned verbatim by the MCP tool */ @JsonProperty("structuredContent") Object structuredContent, /** Provider-neutral source material this tool makes available to the model as citable content. Persisted so it survives session resume. Experimental. */ - @JsonProperty("citableSources") List citableSources + @JsonProperty("citableSources") List citableSources, + /** FIDES IFC label projected from tool ingress metadata (MCP `CallToolResult._meta` or synthesized built-in ingress labels) — persisted as `{ ifc: ... }` (only the `ifc` key, not the whole `_meta`). Persisted so the FIDES IFC label survives session resume: the engine rehydrates accumulated taint by replaying these on load. Populated for ingress sources when FIDES IFC is on. Experimental. */ + @JsonProperty("mcpMeta") Object mcpMeta ) { } diff --git a/java/src/generated/java/com/github/copilot/generated/Verbosity.java b/java/src/generated/java/com/github/copilot/generated/Verbosity.java new file mode 100644 index 0000000000..9db84f1857 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/Verbosity.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; + +/** + * Output verbosity level used for supported model calls (e.g. "low", "medium", "high") + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum Verbosity { + /** The {@code low} variant. */ + LOW("low"), + /** The {@code medium} variant. */ + MEDIUM("medium"), + /** The {@code high} variant. */ + HIGH("high"); + + private final String value; + Verbosity(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static Verbosity fromValue(String value) { + for (Verbosity v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown Verbosity value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/CommandsListResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/CommandsListResult.java new file mode 100644 index 0000000000..9873fa7ff9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/CommandsListResult.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; + +/** + * Slash commands available in the session, after applying any include/exclude filters. + * + * @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 CommandsListResult( + /** Commands available in this session */ + @JsonProperty("commands") List commands +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/DiscoveredCanvas.java b/java/src/generated/java/com/github/copilot/generated/rpc/DiscoveredCanvas.java index e6e02745c8..0b0c518040 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/DiscoveredCanvas.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/DiscoveredCanvas.java @@ -26,6 +26,8 @@ public record DiscoveredCanvas( @JsonProperty("displayName") String displayName, /** Short, single-sentence description shown to the agent in canvas catalogs. */ @JsonProperty("description") String description, + /** Host-local PNG path for the canvas icon, when supplied */ + @JsonProperty("icon") String icon, /** JSON Schema for canvas open input */ @JsonProperty("inputSchema") Object inputSchema, /** Actions the agent or host may invoke on an open instance */ diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/HookInvokeRequest.java b/java/src/generated/java/com/github/copilot/generated/rpc/HookInvokeRequest.java new file mode 100644 index 0000000000..9ed02d28b0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/HookInvokeRequest.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Runtime-owned wire payload for a server-to-client hook callback invocation. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record HookInvokeRequest( + @JsonProperty("sessionId") String sessionId, + @JsonProperty("hookType") HookType hookType, + @JsonProperty("input") Object input +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/HookType.java b/java/src/generated/java/com/github/copilot/generated/rpc/HookType.java new file mode 100644 index 0000000000..006f8a7c1f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/HookType.java @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Hook event name dispatched through the SDK callback transport. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum HookType { + /** The {@code preToolUse} variant. */ + PRETOOLUSE("preToolUse"), + /** The {@code preMcpToolCall} variant. */ + PREMCPTOOLCALL("preMcpToolCall"), + /** The {@code postToolUse} variant. */ + POSTTOOLUSE("postToolUse"), + /** The {@code postToolUseFailure} variant. */ + POSTTOOLUSEFAILURE("postToolUseFailure"), + /** The {@code userPromptSubmitted} variant. */ + USERPROMPTSUBMITTED("userPromptSubmitted"), + /** The {@code sessionStart} variant. */ + SESSIONSTART("sessionStart"), + /** The {@code sessionEnd} variant. */ + SESSIONEND("sessionEnd"), + /** The {@code postResult} variant. */ + POSTRESULT("postResult"), + /** The {@code prePRDescription} variant. */ + PREPRDESCRIPTION("prePRDescription"), + /** The {@code errorOccurred} variant. */ + ERROROCCURRED("errorOccurred"), + /** The {@code agentStop} variant. */ + AGENTSTOP("agentStop"), + /** The {@code subagentStart} variant. */ + SUBAGENTSTART("subagentStart"), + /** The {@code subagentStop} variant. */ + SUBAGENTSTOP("subagentStop"), + /** The {@code preCompact} variant. */ + PRECOMPACT("preCompact"), + /** The {@code permissionRequest} variant. */ + PERMISSIONREQUEST("permissionRequest"), + /** The {@code notification} variant. */ + NOTIFICATION("notification"); + + private final String value; + HookType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static HookType fromValue(String value) { + for (HookType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown HookType value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/HooksInvokeResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/HooksInvokeResult.java new file mode 100644 index 0000000000..a111b7af4e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/HooksInvokeResult.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Optional output returned by an SDK callback hook. + * + * @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 HooksInvokeResult( + @JsonProperty("output") Object output +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/McpResource.java b/java/src/generated/java/com/github/copilot/generated/rpc/McpResource.java new file mode 100644 index 0000000000..92302772ce --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/McpResource.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * 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.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * An MCP resource descriptor (spec `Resource`): URI, name, and optional title, description, MIME type, size, icons, annotations, and metadata. Server-provided fields outside the standard descriptor shape are exposed under `additionalProperties`. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpResource( + /** The resource URI (e.g. ui://... or file:///...) */ + @JsonProperty("uri") String uri, + /** The programmatic name of the resource */ + @JsonProperty("name") String name, + /** Optional human-readable display title */ + @JsonProperty("title") String title, + /** Optional description of what this resource represents */ + @JsonProperty("description") String description, + /** MIME type of the resource, if known */ + @JsonProperty("mimeType") String mimeType, + /** Resource size in bytes, when known */ + @JsonProperty("size") Long size, + /** Icons associated with this resource */ + @JsonProperty("icons") List icons, + /** Model/client annotations associated with this resource */ + @JsonProperty("annotations") McpResourceAnnotations annotations, + /** Resource-level metadata */ + @JsonProperty("_meta") Map meta, + /** Server-provided non-standard descriptor fields preserved from the MCP response */ + @JsonProperty("additionalProperties") Map additionalProperties +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/McpResourceAnnotations.java b/java/src/generated/java/com/github/copilot/generated/rpc/McpResourceAnnotations.java new file mode 100644 index 0000000000..6cae65957a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/McpResourceAnnotations.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 java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Standard MCP resource annotations plus preserved non-standard annotation fields. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpResourceAnnotations( + /** Intended audience roles for this resource */ + @JsonProperty("audience") List audience, + /** Priority hint for model/client use */ + @JsonProperty("priority") Double priority, + /** Last-modified timestamp hint */ + @JsonProperty("lastModified") String lastModified, + /** Server-provided non-standard annotation fields preserved from the MCP response */ + @JsonProperty("additionalProperties") Map additionalProperties +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/McpResourceContent.java b/java/src/generated/java/com/github/copilot/generated/rpc/McpResourceContent.java new file mode 100644 index 0000000000..4967286f15 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/McpResourceContent.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 java.util.Map; +import javax.annotation.processing.Generated; + +/** + * MCP resource content with URI, optional MIME type, text or base64 blob, and resource metadata. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpResourceContent( + /** The resource URI */ + @JsonProperty("uri") String uri, + /** MIME type of the content */ + @JsonProperty("mimeType") String mimeType, + /** Text content (e.g. HTML) */ + @JsonProperty("text") String text, + /** Base64-encoded binary content */ + @JsonProperty("blob") String blob, + /** Resource-level metadata (CSP, permissions, etc.) */ + @JsonProperty("_meta") Map meta +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/McpResourceIcon.java b/java/src/generated/java/com/github/copilot/generated/rpc/McpResourceIcon.java new file mode 100644 index 0000000000..f5a8c68d34 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/McpResourceIcon.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 java.util.Map; +import javax.annotation.processing.Generated; + +/** + * A resource icon descriptor plus preserved non-standard icon fields. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpResourceIcon( + /** Icon URI */ + @JsonProperty("src") String src, + /** Icon MIME type, when known */ + @JsonProperty("mimeType") String mimeType, + /** Icon sizes hint */ + @JsonProperty("sizes") String sizes, + /** Theme hint for this icon */ + @JsonProperty("theme") String theme, + /** Server-provided non-standard icon fields preserved from the MCP response */ + @JsonProperty("additionalProperties") Map additionalProperties +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/McpResourceTemplate.java b/java/src/generated/java/com/github/copilot/generated/rpc/McpResourceTemplate.java new file mode 100644 index 0000000000..14ffca372d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/McpResourceTemplate.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * 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.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * An MCP resource template descriptor (spec `ResourceTemplate`): an RFC 6570 URI template, name, and optional title, description, MIME type, icons, annotations, and metadata. Server-provided fields outside the standard descriptor shape are exposed under `additionalProperties`. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpResourceTemplate( + /** An RFC 6570 URI template for constructing resource URIs */ + @JsonProperty("uriTemplate") String uriTemplate, + /** The programmatic name of the resource template */ + @JsonProperty("name") String name, + /** Optional human-readable display title */ + @JsonProperty("title") String title, + /** Optional description of what this template is for */ + @JsonProperty("description") String description, + /** MIME type for resources matching this template, if uniform */ + @JsonProperty("mimeType") String mimeType, + /** Icons associated with resources matching this template */ + @JsonProperty("icons") List icons, + /** Model/client annotations associated with this template */ + @JsonProperty("annotations") McpResourceAnnotations annotations, + /** Resource-template-level metadata */ + @JsonProperty("_meta") Map meta, + /** Server-provided non-standard descriptor fields preserved from the MCP response */ + @JsonProperty("additionalProperties") Map additionalProperties +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/McpToolUi.java b/java/src/generated/java/com/github/copilot/generated/rpc/McpToolUi.java new file mode 100644 index 0000000000..2f4436ca42 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/McpToolUi.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 java.util.List; +import javax.annotation.processing.Generated; + +/** + * Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpToolUi( + /** URI of the tool's MCP App resource, typically a `ui://` resource identifier. Use `session.mcp.resources.read` to fetch its HTML and resource metadata. */ + @JsonProperty("resourceUri") String resourceUri, + /** Tool visibility advertised by the server. When absent, MCP Apps defaults apply. */ + @JsonProperty("visibility") List visibility +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/McpToolUiVisibility.java b/java/src/generated/java/com/github/copilot/generated/rpc/McpToolUiVisibility.java new file mode 100644 index 0000000000..9e73f0c90c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/McpToolUiVisibility.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; + +/** + * Consumer allowed to call an MCP tool. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpToolUiVisibility { + /** The {@code model} variant. */ + MODEL("model"), + /** The {@code app} variant. */ + APP("app"); + + private final String value; + McpToolUiVisibility(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpToolUiVisibility fromValue(String value) { + for (McpToolUiVisibility v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpToolUiVisibility value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/McpTools.java b/java/src/generated/java/com/github/copilot/generated/rpc/McpTools.java index 04d6881266..37782f6d31 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/McpTools.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/McpTools.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * MCP tool metadata with tool name and optional description. + * MCP tool metadata with tool name, optional description, and normalized MCP Apps discovery metadata. * * @since 1.0.0 */ @@ -24,6 +24,8 @@ public record McpTools( /** Tool name. */ @JsonProperty("name") String name, /** Tool description, when provided. */ - @JsonProperty("description") String description + @JsonProperty("description") String description, + /** Normalized MCP Apps discovery metadata. An empty object indicates that a valid `_meta.ui` block was present without recognized fields. */ + @JsonProperty("ui") McpToolUi ui ) { } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java b/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java index 53ed64f4bf..f8298dd624 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java @@ -26,6 +26,8 @@ public record ModelBilling( /** Token-level pricing information for this model */ @JsonProperty("tokenPrices") ModelBillingTokenPrices tokenPrices, /** Whole-number percentage discount (0-100) applied to usage billed through this model. Populated for the synthetic `auto` model, where requests routed by auto-mode are billed at a reduced rate; absent for concrete models. */ - @JsonProperty("discountPercent") Long discountPercent + @JsonProperty("discountPercent") Long discountPercent, + /** Active server-driven promotion for this model, if any. Present when the model is being promoted with a time-boxed discount. */ + @JsonProperty("promo") ModelBillingPromo promo ) { } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/ModelBillingPromo.java b/java/src/generated/java/com/github/copilot/generated/rpc/ModelBillingPromo.java new file mode 100644 index 0000000000..731e298355 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/ModelBillingPromo.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 javax.annotation.processing.Generated; + +/** + * Active server-driven promotion for a model, including its discount and expiry. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelBillingPromo( + /** Stable identifier for the promotion campaign. */ + @JsonProperty("id") String id, + /** Percentage discount (0-100) applied while the promotion is active. May be fractional. */ + @JsonProperty("discountPercent") Double discountPercent, + /** UTC ISO 8601 timestamp marking when the promotion ends. Always present: the API only surfaces a promo whose expiry parses and is in the future. 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. */ + @JsonProperty("message") String message +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/OpenCanvasInstance.java b/java/src/generated/java/com/github/copilot/generated/rpc/OpenCanvasInstance.java index 9495d21611..f38ba82c4f 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/OpenCanvasInstance.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/OpenCanvasInstance.java @@ -29,6 +29,8 @@ public record OpenCanvasInstance( @JsonProperty("extensionName") String extensionName, /** Provider-local canvas identifier */ @JsonProperty("canvasId") String canvasId, + /** Host-local PNG path for the canvas icon, when supplied */ + @JsonProperty("icon") String icon, /** Rendered title */ @JsonProperty("title") String title, /** Provider-supplied status text */ diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/PluginsMarketplacesAddParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/PluginsMarketplacesAddParams.java index f33ade9a02..f4d00d7c16 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/PluginsMarketplacesAddParams.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/PluginsMarketplacesAddParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Marketplace source to register. + * Marketplace source and optional working directory for relative-path resolution. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -25,6 +25,8 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record PluginsMarketplacesAddParams( /** Marketplace source. Accepts the same forms as the CLI: "owner/repo" or "owner/repo#ref" (GitHub), an http/https/ssh URL (optionally with #ref), a git scp-style URL (user@host:path), or a local path. The marketplace's own name (from its manifest) is used as the registration key. */ - @JsonProperty("source") String source + @JsonProperty("source") String source, + /** Working directory used to resolve relative local paths in `source`. Defaults to the server's current working directory. */ + @JsonProperty("workingDirectory") String workingDirectory ) { } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SendMessageItem.java b/java/src/generated/java/com/github/copilot/generated/rpc/SendMessageItem.java new file mode 100644 index 0000000000..bdb37cd9fd --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SendMessageItem.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 java.util.List; +import javax.annotation.processing.Generated; + +/** + * A single user message to append to the session as part of a `session.sendMessages` turn + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SendMessageItem( + /** The user message text */ + @JsonProperty("prompt") String prompt, + /** If provided, this is shown in the timeline instead of `prompt` */ + @JsonProperty("displayPrompt") String displayPrompt, + /** Optional attachments (files, directories, selections, blobs, GitHub references) to include with this message */ + @JsonProperty("attachments") List attachments, + /** If false, this message will not trigger a Premium Request Unit charge. User messages default to billable. */ + @JsonProperty("billable") Boolean billable, + /** If set, the request will fail if the named tool is not available when this message is among the user messages at the start of the current exchange */ + @JsonProperty("requiredTool") String requiredTool, + /** Optional provenance tag copied to the resulting user.message event. Must match one of three forms: the literal `system`, `command-` for messages originating from a command (e.g. slash command, Mission Control command), or `schedule-` for messages originating from a scheduled job. */ + @JsonProperty("source") String source +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/ServerCommandsApi.java b/java/src/generated/java/com/github/copilot/generated/rpc/ServerCommandsApi.java new file mode 100644 index 0000000000..efa5317ea4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/ServerCommandsApi.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 commands} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerCommandsApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerCommandsApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Slash commands available in the session, after applying any include/exclude filters. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture list() { + return caller.invoke("commands.list", java.util.Map.of(), CommandsListResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsMarketplacesApi.java b/java/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsMarketplacesApi.java index 3d8ced5848..47b239a0c0 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsMarketplacesApi.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsMarketplacesApi.java @@ -38,7 +38,7 @@ public CompletableFuture list() { } /** - * Marketplace source to register. + * Marketplace source and optional working directory for relative-path resolution. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java b/java/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java index 21907b7ab3..033fe8bf3e 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java @@ -43,6 +43,8 @@ public final class ServerRpc { public final ServerAgentsApi agents; /** API methods for the {@code instructions} namespace. */ public final ServerInstructionsApi instructions; + /** API methods for the {@code commands} namespace. */ + public final ServerCommandsApi commands; /** API methods for the {@code user} namespace. */ public final ServerUserApi user; /** API methods for the {@code runtime} namespace. */ @@ -72,6 +74,7 @@ public ServerRpc(RpcCaller caller) { this.skills = new ServerSkillsApi(caller); this.agents = new ServerAgentsApi(caller); this.instructions = new ServerInstructionsApi(caller); + this.commands = new ServerCommandsApi(caller); this.user = new ServerUserApi(caller); this.runtime = new ServerRuntimeApi(caller); this.sessionFs = new ServerSessionFsApi(caller); diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasOpenResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasOpenResult.java index 1d4e0bdf53..7678d1d6a8 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasOpenResult.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasOpenResult.java @@ -32,6 +32,8 @@ public record SessionCanvasOpenResult( @JsonProperty("extensionName") String extensionName, /** Provider-local canvas identifier */ @JsonProperty("canvasId") String canvasId, + /** Host-local PNG path for the canvas icon, when supplied */ + @JsonProperty("icon") String icon, /** Rendered title */ @JsonProperty("title") String title, /** Provider-supplied status text */ diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionEventLogRegisterInterestParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionEventLogRegisterInterestParams.java index d6f522ed12..567156cc5e 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionEventLogRegisterInterestParams.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionEventLogRegisterInterestParams.java @@ -26,7 +26,7 @@ public record SessionEventLogRegisterInterestParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, - /** The event type the consumer wants the runtime to treat as 'observed' for behavior-switching gating. Some runtime code paths inspect whether any consumer is interested in a specific event type and choose a different implementation accordingly (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates the full interactive OAuth flow to the consumer; when no interest is registered the runtime installs a browserless fallback that silently reuses cached tokens). SDK clients that long-poll events do NOT automatically appear as listeners to these gating checks — they must explicitly call `registerInterest` for each event type they want the runtime to count as having a consumer. Multiple registrations for the same event type from the same or different consumers are tracked independently and must each be released. See: `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, `session_limits_exhausted.requested`, `user_input.requested`, `elicitation.requested`, `command.queued`, `exit_plan_mode.requested`. */ + /** The event type the consumer wants the runtime to treat as 'observed' for behavior-switching gating. Some runtime code paths inspect whether any consumer is interested in a specific event type and choose a different implementation accordingly (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates interactive OAuth token acquisition to the consumer via `mcp.oauth_required` events; when no interest is registered the runtime still attempts non-interactive reconnect from cached or refreshable tokens, and only marks the server `needs-auth` if usable credentials are unavailable — it does not open a browser or start interactive OAuth without a consumer). SDK clients that long-poll events do NOT automatically appear as listeners to these gating checks — they must explicitly call `registerInterest` for each event type they want the runtime to count as having a consumer. Multiple registrations for the same event type from the same or different consumers are tracked independently and must each be released. See: `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, `session_limits_exhausted.requested`, `user_input.requested`, `elicitation.requested`, `command.queued`, `exit_plan_mode.requested`. */ @JsonProperty("eventType") String eventType ) { } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java index f4603c249f..172f6e5075 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java @@ -30,6 +30,8 @@ public final class SessionMcpApi { public final SessionMcpHeadersApi headers; /** API methods for the {@code mcp.apps} sub-namespace. */ public final SessionMcpAppsApi apps; + /** API methods for the {@code mcp.resources} sub-namespace. */ + public final SessionMcpResourcesApi resources; /** @param caller the RPC transport function */ SessionMcpApi(RpcCaller caller, String sessionId) { @@ -38,6 +40,7 @@ public final class SessionMcpApi { this.oauth = new SessionMcpOauthApi(caller, sessionId); this.headers = new SessionMcpHeadersApi(caller, sessionId); this.apps = new SessionMcpAppsApi(caller, sessionId); + this.resources = new SessionMcpResourcesApi(caller, sessionId); } /** @@ -202,7 +205,7 @@ public CompletableFuture configureGitHub(Sessio } /** - * Server name and opaque configuration for an individual MCP server start. + * Server name and configuration for an individual MCP server start. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -218,7 +221,7 @@ public CompletableFuture startServer(SessionMcpStartServerParams params) { } /** - * Server name and opaque configuration for an individual MCP server restart. + * Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesApi.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesApi.java new file mode 100644 index 0000000000..c1a30e135d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesApi.java @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * 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 mcp.resources} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionMcpResourcesApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionMcpResourcesApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * MCP server and resource URI to fetch. + *

+ * 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 read(SessionMcpResourcesReadParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.resources.read", _p, SessionMcpResourcesReadResult.class); + } + + /** + * MCP server whose resources to enumerate. + *

+ * 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 list(SessionMcpResourcesListParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.resources.list", _p, SessionMcpResourcesListResult.class); + } + + /** + * MCP server whose resource templates to enumerate. + *

+ * 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 listTemplates(SessionMcpResourcesListTemplatesParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.resources.listTemplates", _p, SessionMcpResourcesListTemplatesResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesListParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesListParams.java new file mode 100644 index 0000000000..bd8a9de64b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesListParams.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; + +/** + * MCP server whose resources to enumerate. + * + * @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 SessionMcpResourcesListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Name of the MCP server whose resources to enumerate */ + @JsonProperty("serverName") String serverName, + /** Opaque MCP pagination cursor from a prior `nextCursor` value */ + @JsonProperty("cursor") String cursor +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesListResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesListResult.java new file mode 100644 index 0000000000..b7e1042cfc --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesListResult.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; + +/** + * One page of resources advertised by the named MCP server. + * + * @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 SessionMcpResourcesListResult( + /** Resources advertised by the server (proxied MCP `resources/list`) */ + @JsonProperty("resources") List resources, + /** Opaque cursor for the next page, if the server has more resources */ + @JsonProperty("nextCursor") String nextCursor +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesListTemplatesParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesListTemplatesParams.java new file mode 100644 index 0000000000..a58252c766 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesListTemplatesParams.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; + +/** + * MCP server whose resource templates to enumerate. + * + * @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 SessionMcpResourcesListTemplatesParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Name of the MCP server whose resource templates to enumerate */ + @JsonProperty("serverName") String serverName, + /** Opaque MCP pagination cursor from a prior `nextCursor` value */ + @JsonProperty("cursor") String cursor +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesListTemplatesResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesListTemplatesResult.java new file mode 100644 index 0000000000..9cb3ff0569 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesListTemplatesResult.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; + +/** + * One page of resource templates advertised by the named MCP server. + * + * @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 SessionMcpResourcesListTemplatesResult( + /** Resource templates advertised by the server (proxied MCP `resources/templates/list`) */ + @JsonProperty("resourceTemplates") List resourceTemplates, + /** Opaque cursor for the next page, if the server has more resource templates */ + @JsonProperty("nextCursor") String nextCursor +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesReadParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesReadParams.java new file mode 100644 index 0000000000..5c7b9d803b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesReadParams.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; + +/** + * MCP server and resource URI to fetch. + * + * @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 SessionMcpResourcesReadParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Name of the MCP server hosting the resource */ + @JsonProperty("serverName") String serverName, + /** Resource URI */ + @JsonProperty("uri") String uri +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesReadResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesReadResult.java new file mode 100644 index 0000000000..7e85574ee5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesReadResult.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; + +/** + * Resource contents returned by the MCP server. + * + * @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 SessionMcpResourcesReadResult( + /** Resource contents returned by the server */ + @JsonProperty("contents") List contents +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpRestartServerParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpRestartServerParams.java index 12035ae070..b517802562 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpRestartServerParams.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpRestartServerParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Server name and opaque configuration for an individual MCP server restart. + * Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -28,7 +28,7 @@ public record SessionMcpRestartServerParams( @JsonProperty("sessionId") String sessionId, /** Name of the MCP server to restart */ @JsonProperty("serverName") String serverName, - /** Opaque server configuration (MCPServerConfig). Marked internal: an in-process runtime shape supplied only by in-process CLI callers. */ + /** Replacement MCP server configuration (stdio process or remote HTTP/SSE). Omit to restart the server with its already-registered configuration (config-free restart-by-name). */ @JsonProperty("config") Object config ) { } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpStartServerParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpStartServerParams.java index 6e866d64d7..e4c14e30e4 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpStartServerParams.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpStartServerParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Server name and opaque configuration for an individual MCP server start. + * Server name and configuration for an individual MCP server start. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -28,7 +28,7 @@ public record SessionMcpStartServerParams( @JsonProperty("sessionId") String sessionId, /** Name of the MCP server to start */ @JsonProperty("serverName") String serverName, - /** Opaque server configuration (MCPServerConfig). Marked internal: an in-process runtime shape supplied only by in-process CLI callers. */ + /** MCP server configuration (stdio process or remote HTTP/SSE) */ @JsonProperty("config") Object config ) { } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataApi.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataApi.java index 209fe8cac4..0b15df5d43 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataApi.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataApi.java @@ -123,7 +123,7 @@ public CompletableFuture recordContextChange(SessionMetadataRecordContextC } /** - * Absolute path to set as the session's new working directory. + * Absolute path to set as the session's new working directory. For local sessions the path must be absolute and exist on disk: it is validated before any session state changes, and a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote sessions record the path as-is. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataSetWorkingDirectoryParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataSetWorkingDirectoryParams.java index 99ab15b9ba..968cf5d8b6 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataSetWorkingDirectoryParams.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataSetWorkingDirectoryParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Absolute path to set as the session's new working directory. + * Absolute path to set as the session's new working directory. For local sessions the path must be absolute and exist on disk: it is validated before any session state changes, and a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote sessions record the path as-is. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataSetWorkingDirectoryResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataSetWorkingDirectoryResult.java index 477ba62bea..b0dff14ed1 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataSetWorkingDirectoryResult.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataSetWorkingDirectoryResult.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for `process.chdir` and any related side-effects (file index, etc.); this method only updates the session's own recorded path. + * Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects (file index, etc.); it does NOT change the process working directory (a session's cwd is per-session, not process-global). For local sessions the runtime validates the target first (an absolute path that exists on disk) and re-bases the permission primary directory; a rejected validation fails the call before anything is mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the new directory (best-effort). Remote sessions only record the path. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionModelListResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionModelListResult.java index f3fd591f62..8951499ef4 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionModelListResult.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionModelListResult.java @@ -28,6 +28,8 @@ public record SessionModelListResult( /** Available models, ordered with the most preferred default first. Includes both Copilot (CAPI) models and any registry BYOK models; a BYOK model appears under its provider-qualified selection id (`provider/id`). */ @JsonProperty("list") List list, + /** Cost categories for the full CAPI catalog, including picker-disabled models that Auto may select. Metadata only; entries absent from `list` are not manually selectable. */ + @JsonProperty("modelPriceCategories") List modelPriceCategories, /** Per-quota snapshots returned alongside the model list, keyed by quota type. */ @JsonProperty("quotaSnapshots") Map quotaSnapshots ) { diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionModelPriceCategory.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionModelPriceCategory.java new file mode 100644 index 0000000000..295f6a01f0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionModelPriceCategory.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; + +/** + * Cost-category metadata for a CAPI model. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionModelPriceCategory( + @JsonProperty("id") String id, + @JsonProperty("priceCategory") ModelPickerPriceCategory priceCategory +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java index b0b69ff25e..580ab3bab2 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java @@ -32,6 +32,8 @@ public record SessionModelSwitchToParams( @JsonProperty("reasoningEffort") String reasoningEffort, /** Reasoning summary mode to request for supported model clients */ @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, + /** Output verbosity level to request for supported models */ + @JsonProperty("verbosity") Verbosity verbosity, /** Override individual model capabilities resolved by the runtime */ @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. */ diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java index 9cede57d35..fb8c25b3da 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java @@ -36,6 +36,8 @@ public record SessionOptionsUpdateParams( @JsonProperty("reasoningEffort") String reasoningEffort, /** Reasoning summary mode for supported model clients. */ @JsonProperty("reasoningSummary") OptionsUpdateReasoningSummary reasoningSummary, + /** Output verbosity level for supported models. */ + @JsonProperty("verbosity") Verbosity verbosity, /** Identifier of the client driving the session. */ @JsonProperty("clientName") String clientName, /** Identifier sent to LSP-style integrations. */ @@ -56,6 +58,8 @@ public record SessionOptionsUpdateParams( @JsonProperty("availableTools") List availableTools, /** Denylist of tool names for this session. */ @JsonProperty("excludedTools") List excludedTools, + /** 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. Set to null to remove the allowlist restriction. */ + @JsonProperty("includedBuiltinAgents") List includedBuiltinAgents, /** Built-in subagent names to exclude from this session. Excluded built-ins are hidden from agent discovery and cannot be dispatched unless a custom agent with the same name is available. */ @JsonProperty("excludedBuiltinAgents") List excludedBuiltinAgents, /** Controls how availableTools (allowlist) and excludedTools (denylist) combine when both are set. */ diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateResult.java index a13e0d0d5e..3d7d274610 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateResult.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateResult.java @@ -25,6 +25,8 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record SessionOptionsUpdateResult( /** Whether the operation succeeded */ - @JsonProperty("success") Boolean success + @JsonProperty("success") Boolean success, + /** Number of hooks loaded from installed plugins, returned when installedPlugins is updated */ + @JsonProperty("pluginHookCount") Long pluginHookCount ) { } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java index 1007c0db7b..c150b75679 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java @@ -173,6 +173,22 @@ public CompletableFuture send(SessionSendParams params) { return caller.invoke("session.send", _p, SessionSendResult.class); } + /** + * Parameters for sending zero or more user messages to the session in a single turn. Remote-backed (Mission Control) sessions do not support this method and will return an error. + *

+ * 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 sendMessages(SessionSendMessagesParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.sendMessages", _p, SessionSendMessagesResult.class); + } + /** * Parameters for aborting the current turn *

diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionSendMessagesParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionSendMessagesParams.java new file mode 100644 index 0000000000..2d66b4fe16 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionSendMessagesParams.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * 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 java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Parameters for sending zero or more user messages to the session in a single turn. Remote-backed (Mission Control) sessions do not support this method and will return an error. + * + * @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 SessionSendMessagesParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The user messages to append to the conversation, in order. May be empty, in which case a single turn runs over the existing history with no new user message. */ + @JsonProperty("messages") List messages, + /** How to deliver the messages. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. */ + @JsonProperty("mode") SendMode mode, + /** If true, adds the messages to the front of the queue instead of the end */ + @JsonProperty("prepend") Boolean prepend, + /** The UI mode the agent was in when these messages were sent. Defaults to the session's current mode. */ + @JsonProperty("agentMode") SendAgentMode agentMode, + /** Custom HTTP headers to include in outbound model requests for this turn. Merged with session-level provider headers; per-turn headers augment and overwrite session-level headers with the same key. */ + @JsonProperty("requestHeaders") Map requestHeaders, + /** W3C Trace Context traceparent header for distributed tracing of this agent turn */ + @JsonProperty("traceparent") String traceparent, + /** W3C Trace Context tracestate header for distributed tracing */ + @JsonProperty("tracestate") String tracestate, + /** If true, await completion of the agentic loop for this turn before returning. Defaults to false (fire-and-forget). When true, the result still contains the same `messageIds`; the caller can rely on the agent having processed the messages before the call resolves. */ + @JsonProperty("wait") Boolean wait_ +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionSendMessagesResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionSendMessagesResult.java new file mode 100644 index 0000000000..aeb556ba0f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionSendMessagesResult.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; + +/** + * Result of sending zero or more user messages + * + * @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 SessionSendMessagesResult( + /** Unique identifiers assigned to the messages, one per provided message in order. Empty when no messages were provided. */ + @JsonProperty("messageIds") List messageIds +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandAgentPromptResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandAgentPromptResult.java index a034458c98..4c454a55a9 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandAgentPromptResult.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandAgentPromptResult.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * Slash-command invocation result that submits an agent prompt, with display prompt, optional mode, and settings-change flag. + * Slash-command invocation result that submits an agent prompt, with display prompt, optional mode, optional user-facing notice, and settings-change flag. * * @since 1.0.0 */ @@ -40,6 +40,10 @@ public final class SlashCommandAgentPromptResult extends SlashCommandInvocationR @JsonProperty("mode") private SessionMode mode; + /** Optional user-facing notice to show before the prompt is submitted */ + @JsonProperty("notice") + private String notice; + /** True when the invocation mutated user runtime settings; consumers caching settings should refresh */ @JsonProperty("runtimeSettingsChanged") private Boolean runtimeSettingsChanged; @@ -53,6 +57,9 @@ public final class SlashCommandAgentPromptResult extends SlashCommandInvocationR public SessionMode getMode() { return mode; } public void setMode(SessionMode mode) { this.mode = mode; } + public String getNotice() { return notice; } + public void setNotice(String notice) { this.notice = notice; } + public Boolean getRuntimeSettingsChanged() { return runtimeSettingsChanged; } public void setRuntimeSettingsChanged(Boolean runtimeSettingsChanged) { this.runtimeSettingsChanged = runtimeSettingsChanged; } } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/Verbosity.java b/java/src/generated/java/com/github/copilot/generated/rpc/Verbosity.java new file mode 100644 index 0000000000..188ce23b41 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/Verbosity.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; + +/** + * Output verbosity level for supported models + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum Verbosity { + /** The {@code low} variant. */ + LOW("low"), + /** The {@code medium} variant. */ + MEDIUM("medium"), + /** The {@code high} variant. */ + HIGH("high"); + + private final String value; + Verbosity(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static Verbosity fromValue(String value) { + for (Verbosity v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown Verbosity value: " + value); + } +} diff --git a/java/src/main/java/com/github/copilot/CopilotClient.java b/java/src/main/java/com/github/copilot/CopilotClient.java index b90ccd545a..7244c8c0a5 100644 --- a/java/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/src/main/java/com/github/copilot/CopilotClient.java @@ -926,6 +926,7 @@ CompletableFuture updateSessionOptionsForMode(CopilotSession session, Bool null, // modelCapabilitiesOverrides null, // reasoningEffort null, // reasoningSummary + null, // verbosity null, // clientName null, // lspClientName null, // integrationId @@ -936,6 +937,7 @@ CompletableFuture updateSessionOptionsForMode(CopilotSession session, Bool null, // workingDirectory null, // availableTools null, // excludedTools + null, // includedBuiltinAgents null, // excludedBuiltinAgents null, // toolFilterPrecedence null, // enableScriptSafety diff --git a/java/src/main/java/com/github/copilot/CopilotRequestContext.java b/java/src/main/java/com/github/copilot/CopilotRequestContext.java index 0948a24c28..610fb56c6d 100644 --- a/java/src/main/java/com/github/copilot/CopilotRequestContext.java +++ b/java/src/main/java/com/github/copilot/CopilotRequestContext.java @@ -22,6 +22,12 @@ public final class CopilotRequestContext { private final String requestId; @Nullable private final String sessionId; + @Nullable + private final String agentId; + @Nullable + private final String parentAgentId; + @Nullable + private final String interactionType; private final CopilotRequestTransport transport; private final String url; private final Map> headers; @@ -29,20 +35,25 @@ public final class CopilotRequestContext { private LlmWebSocketResponseBridge webSocketResponse; - CopilotRequestContext(String requestId, @Nullable String sessionId, CopilotRequestTransport transport, String url, - Map> headers, CompletableFuture cancellation) { + CopilotRequestContext(String requestId, @Nullable String sessionId, @Nullable String agentId, + @Nullable String parentAgentId, @Nullable String interactionType, CopilotRequestTransport transport, + String url, Map> headers, CompletableFuture cancellation) { this.requestId = requestId; this.sessionId = sessionId; + this.agentId = agentId; + this.parentAgentId = parentAgentId; + this.interactionType = interactionType; this.transport = transport; this.url = url; this.headers = headers; this.cancellation = cancellation; } - private CopilotRequestContext(String requestId, @Nullable String sessionId, CopilotRequestTransport transport, + private CopilotRequestContext(String requestId, @Nullable String sessionId, @Nullable String agentId, + @Nullable String parentAgentId, @Nullable String interactionType, CopilotRequestTransport transport, String url, Map> headers, CompletableFuture cancellation, LlmWebSocketResponseBridge webSocketResponse) { - this(requestId, sessionId, transport, url, headers, cancellation); + this(requestId, sessionId, agentId, parentAgentId, interactionType, transport, url, headers, cancellation); this.webSocketResponse = webSocketResponse; } @@ -68,6 +79,39 @@ public String sessionId() { return sessionId; } + /** + * Gets the stable per-agent-instance id for the agent trajectory that issued + * this request, or {@code null} when no agent is in scope. + * + * @return the agent id, or {@code null} + */ + @Nullable + public String agentId() { + return agentId; + } + + /** + * Gets the id of the parent agent when this request was issued by a subagent, + * or {@code null} for root-agent and non-agent requests. + * + * @return the parent agent id, or {@code null} + */ + @Nullable + public String parentAgentId() { + return parentAgentId; + } + + /** + * Gets the runtime classification for the interaction that produced this + * request, or {@code null} when the runtime did not classify it. + * + * @return the interaction type, or {@code null} + */ + @Nullable + public String interactionType() { + return interactionType; + } + /** * Gets the transport the runtime would otherwise use. * @@ -103,8 +147,8 @@ public Map> headers() { * @return the copied context */ public CopilotRequestContext withUrl(String url) { - return new CopilotRequestContext(requestId, sessionId, transport, url, headers, cancellation, - webSocketResponse); + return new CopilotRequestContext(requestId, sessionId, agentId, parentAgentId, interactionType, transport, url, + headers, cancellation, webSocketResponse); } /** @@ -115,8 +159,8 @@ public CopilotRequestContext withUrl(String url) { * @return the copied context */ public CopilotRequestContext withHeaders(Map> headers) { - return new CopilotRequestContext(requestId, sessionId, transport, url, headers, cancellation, - webSocketResponse); + return new CopilotRequestContext(requestId, sessionId, agentId, parentAgentId, interactionType, transport, url, + headers, cancellation, webSocketResponse); } /** diff --git a/java/src/main/java/com/github/copilot/CopilotSession.java b/java/src/main/java/com/github/copilot/CopilotSession.java index 5a597dcd9a..4826b3309f 100644 --- a/java/src/main/java/com/github/copilot/CopilotSession.java +++ b/java/src/main/java/com/github/copilot/CopilotSession.java @@ -158,6 +158,13 @@ public final class CopilotSession implements AutoCloseable { private static final Logger LOG = Logger.getLogger(CopilotSession.class.getName()); private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + /** + * Fixed name of the runtime's built-in tool-search tool. A client can replace + * its behavior by registering a tool with this exact name and + * {@code overridesBuiltInTool} set to {@code true}. + */ + private static final String TOOL_SEARCH_TOOL_NAME = "tool_search_tool"; + /** * The current active session ID. Initialized to the pre-generated value and may * be updated after session.create / session.resume if the server returns a @@ -898,6 +905,32 @@ private void handleBroadcastEventAsync(SessionEvent event) { } } + /** + * Populates the invocation's available-tools snapshot when it targets the + * 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}). + * + * @param toolName + * the name of the tool being invoked + * @param invocation + * the invocation to populate in place + */ + void populateToolSearchMetadata(String toolName, com.github.copilot.rpc.ToolInvocation invocation) { + if (!TOOL_SEARCH_TOOL_NAME.equals(toolName)) { + return; + } + try { + var metadata = getRpc().tools.getCurrentMetadata().join(); + invocation.setAvailableTools(metadata.tools()); + } catch (Exception e) { + LOG.log(Level.FINE, "Failed to fetch tool metadata for tool search", e); + } + } + /** * Executes a tool handler and sends the result back via * {@code session.tools.handlePendingToolCall}. @@ -912,6 +945,8 @@ 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); + tool.handler().invoke(invocation).thenAccept(result -> { try { ToolResultObject toolResult; @@ -1564,7 +1599,7 @@ private void updateOpenCanvasesFromEvent(SessionEvent event) { return; } upsertOpenCanvas(new OpenCanvasInstance(data.instanceId(), data.extensionId(), data.extensionName(), - data.canvasId(), data.title(), data.status(), data.url(), data.input())); + data.canvasId(), data.icon(), data.title(), data.status(), data.url(), data.input())); } } @@ -1910,12 +1945,12 @@ public CompletableFuture abort() { * preserved. * *

{@code
-     * session.setModel("gpt-4.1").get();
+     * session.setModel("gpt-5.4").get();
      * session.setModel("claude-sonnet-4.6", "high").get();
      * }
* * @param model - * the model ID to switch to (e.g., {@code "gpt-4.1"}) + * the model ID to switch to (e.g., {@code "gpt-5.4"}) * @param reasoningEffort * reasoning effort level (e.g., {@code "low"}, {@code "medium"}, * {@code "high"}, {@code "xhigh"}); {@code null} to use default @@ -1927,7 +1962,7 @@ public CompletableFuture abort() { public CompletableFuture setModel(String model, String reasoningEffort) { ensureNotTerminated(); return getRpc().model - .switchTo(new SessionModelSwitchToParams(sessionId, model, reasoningEffort, null, null, null)) + .switchTo(new SessionModelSwitchToParams(sessionId, model, reasoningEffort, null, null, null, null)) .thenApply(r -> null); } @@ -1945,7 +1980,7 @@ public CompletableFuture setModel(String model, String reasoningEffort) { * } * * @param model - * the model ID to switch to (e.g., {@code "gpt-4.1"}) + * the model ID to switch to (e.g., {@code "gpt-5.4"}) * @param reasoningEffort * reasoning effort level (e.g., {@code "low"}, {@code "medium"}, * {@code "high"}, {@code "xhigh"}); {@code null} to use default @@ -1970,7 +2005,7 @@ public CompletableFuture setModel(String model, String reasoningEffort, * preserved. * * @param model - * the model ID to switch to (e.g., {@code "gpt-4.1"}) + * the model ID to switch to (e.g., {@code "gpt-5.4"}) * @param reasoningEffort * reasoning effort level; {@code null} to use default * @param reasoningSummary @@ -2008,7 +2043,7 @@ public CompletableFuture setModel(String model, String reasoningEffort, St ? null : com.github.copilot.generated.rpc.ReasoningSummary.fromValue(reasoningSummary); return getRpc().model.switchTo(new SessionModelSwitchToParams(sessionId, model, reasoningEffort, - generatedReasoningSummary, generatedCapabilities, null)).thenApply(r -> null); + generatedReasoningSummary, null, generatedCapabilities, null)).thenApply(r -> null); } /** @@ -2018,11 +2053,11 @@ public CompletableFuture setModel(String model, String reasoningEffort, St * preserved. * *
{@code
-     * session.setModel("gpt-4.1").get();
+     * session.setModel("gpt-5.4").get();
      * }
* * @param model - * the model ID to switch to (e.g., {@code "gpt-4.1"}) + * the model ID to switch to (e.g., {@code "gpt-5.4"}) * @return a future that completes when the model switch is acknowledged * @throws IllegalStateException * if this session has been terminated diff --git a/java/src/main/java/com/github/copilot/LlmInferenceAdapter.java b/java/src/main/java/com/github/copilot/LlmInferenceAdapter.java index 9087df6c1f..3e741a56bc 100644 --- a/java/src/main/java/com/github/copilot/LlmInferenceAdapter.java +++ b/java/src/main/java/com/github/copilot/LlmInferenceAdapter.java @@ -65,6 +65,9 @@ private LlmInferenceExchange getOrCreateExchange(String requestId) { private void handleRequestStart(JsonRpcClient rpc, String rpcId, JsonNode params) { String requestId = params.get("requestId").asText(); String sessionId = textOrNull(params, "sessionId"); + String agentId = textOrNull(params, "agentId"); + String parentAgentId = textOrNull(params, "parentAgentId"); + String interactionType = textOrNull(params, "interactionType"); String method = textOrNull(params, "method"); String url = textOrNull(params, "url"); CopilotRequestTransport transport = CopilotRequestTransport.fromWire(textOrNull(params, "transport")); @@ -74,8 +77,8 @@ private void handleRequestStart(JsonRpcClient rpc, String rpcId, JsonNode params // body — rather than dropping those frames. LlmInferenceExchange exchange = getOrCreateExchange(requestId); exchange.setMethod(method); - exchange.setContext( - new CopilotRequestContext(requestId, sessionId, transport, url, headers, exchange.cancellation())); + exchange.setContext(new CopilotRequestContext(requestId, sessionId, agentId, parentAgentId, interactionType, + transport, url, headers, exchange.cancellation())); // Return from httpRequestStart immediately (after registering state) so the // runtime's RPC reply is not gated on the consumer's I/O. The actual handler diff --git a/java/src/main/java/com/github/copilot/RpcHandlerDispatcher.java b/java/src/main/java/com/github/copilot/RpcHandlerDispatcher.java index 9a42a8e22d..d2dff958dc 100644 --- a/java/src/main/java/com/github/copilot/RpcHandlerDispatcher.java +++ b/java/src/main/java/com/github/copilot/RpcHandlerDispatcher.java @@ -162,6 +162,8 @@ private void handleToolCall(JsonRpcClient rpc, String requestId, JsonNode params var invocation = new ToolInvocation().setSessionId(sessionId).setToolCallId(toolCallId) .setToolName(toolName).setArguments(arguments); + session.populateToolSearchMetadata(toolName, invocation); + tool.handler().invoke(invocation).thenAccept(result -> { try { ToolResultObject toolResult; diff --git a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java index f88bf2a85e..57d9a46a21 100644 --- a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -145,6 +145,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setInstructionDirectories(config.getInstructionDirectories()); request.setPluginDirectories(config.getPluginDirectories()); request.setLargeOutput(config.getLargeOutput()); + request.setToolSearch(config.getToolSearch()); request.setMemory(config.getMemory()); request.setDisabledSkills(config.getDisabledSkills()); request.setConfigDirectory(config.getConfigDirectory()); @@ -185,6 +186,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setRemoteSession(config.getRemoteSession()); request.setCloud(config.getCloud()); request.setExpAssignments(config.getExpAssignments()); + config.getEnableManagedSettings().ifPresent(request::setEnableManagedSettings); return request; } @@ -280,6 +282,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setInstructionDirectories(config.getInstructionDirectories()); request.setPluginDirectories(config.getPluginDirectories()); request.setLargeOutput(config.getLargeOutput()); + request.setToolSearch(config.getToolSearch()); request.setMemory(config.getMemory()); request.setDisabledSkills(config.getDisabledSkills()); request.setInfiniteSessions(config.getInfiniteSessions()); @@ -306,6 +309,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setGitHubToken(config.getGitHubToken()); request.setRemoteSession(config.getRemoteSession()); request.setExpAssignments(config.getExpAssignments()); + config.getEnableManagedSettings().ifPresent(request::setEnableManagedSettings); return request; } diff --git a/java/src/main/java/com/github/copilot/package-info.java b/java/src/main/java/com/github/copilot/package-info.java index 71025f07af..0e0b2cf824 100644 --- a/java/src/main/java/com/github/copilot/package-info.java +++ b/java/src/main/java/com/github/copilot/package-info.java @@ -31,7 +31,7 @@ * try (var client = new CopilotClient()) { * client.start().get(); * - * var session = client.createSession(new SessionConfig().setModel("gpt-4.1")).get(); + * var session = client.createSession(new SessionConfig().setModel("gpt-5.4")).get(); * * session.on(AssistantMessageEvent.class, msg -> { * System.out.println(msg.getData().content()); diff --git a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index 2510b0bd6e..f2ce097a13 100644 --- a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -138,6 +138,9 @@ public final class CreateSessionRequest { @JsonProperty("largeOutput") private LargeToolOutputConfig largeOutput; + @JsonProperty("toolSearch") + private ToolSearchConfig toolSearch; + @JsonProperty("memory") private MemoryConfiguration memory; @@ -212,6 +215,10 @@ public final class CreateSessionRequest { @JsonProperty("expAssignments") private JsonNode expAssignments; + @JsonProperty("enableManagedSettings") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean enableManagedSettings; + /** Gets the model name. @return the model */ public String getModel() { return model; @@ -620,6 +627,16 @@ public void setLargeOutput(LargeToolOutputConfig largeOutput) { this.largeOutput = largeOutput; } + /** Gets tool-search config. @return the tool-search config */ + public ToolSearchConfig getToolSearch() { + return toolSearch; + } + + /** Sets tool-search config. @param toolSearch the tool-search config */ + public void setToolSearch(ToolSearchConfig toolSearch) { + this.toolSearch = toolSearch; + } + /** Gets memory config. @return the memory config */ public MemoryConfiguration getMemory() { return memory; @@ -966,4 +983,27 @@ public JsonNode getExpAssignments() { public void setExpAssignments(JsonNode expAssignments) { this.expAssignments = expAssignments; } + + /** + * Gets the self-fetch managed settings flag. @return the flag, or {@code null} + * if not set + */ + public Boolean getEnableManagedSettings() { + return enableManagedSettings; + } + + /** + * Sets the self-fetch managed settings flag. @param enableManagedSettings the + * flag + */ + public void setEnableManagedSettings(boolean enableManagedSettings) { + this.enableManagedSettings = enableManagedSettings; + } + + /** + * Clears the enableManagedSettings setting, reverting to the default behavior. + */ + public void clearEnableManagedSettings() { + this.enableManagedSettings = null; + } } diff --git a/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java b/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java index 5136f77786..3604a1ef5d 100644 --- a/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java @@ -63,6 +63,9 @@ public class CustomAgentConfig { @JsonProperty("model") private String model; + @JsonProperty("reasoningEffort") + private String reasoningEffort; + /** * Gets the unique identifier name for this agent. * @@ -282,4 +285,28 @@ public CustomAgentConfig setModel(String model) { this.model = model; return this; } + + /** + * Gets the reasoning effort level for this agent's model. + * + * @return the reasoning effort level, or {@code null} if not set + */ + public String getReasoningEffort() { + return reasoningEffort; + } + + /** + * Sets the reasoning effort level for this agent's model. + *

+ * When omitted, no per-agent override is sent and the backend chooses its + * default. The parent session effort is not inherited. + * + * @param reasoningEffort + * the reasoning effort level + * @return this config for method chaining + */ + public CustomAgentConfig setReasoningEffort(String reasoningEffort) { + this.reasoningEffort = reasoningEffort; + return this; + } } diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index 83b365a410..6f3c315658 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -90,6 +90,7 @@ public class ResumeSessionConfig { private List instructionDirectories; private List pluginDirectories; private LargeToolOutputConfig largeOutput; + private ToolSearchConfig toolSearch; private MemoryConfiguration memory; private List disabledSkills; private InfiniteSessionConfig infiniteSessions; @@ -102,6 +103,7 @@ public class ResumeSessionConfig { private String gitHubToken; private String remoteSession; private JsonNode expAssignments; + private Boolean enableManagedSettings; /** * Gets the AI model to use. @@ -1446,6 +1448,27 @@ public ResumeSessionConfig setLargeOutput(LargeToolOutputConfig largeOutput) { return this; } + /** + * Gets the tool-search configuration. + * + * @return the tool-search config, or {@code null} for the runtime default + */ + public ToolSearchConfig getToolSearch() { + return toolSearch; + } + + /** + * Sets the tool-search configuration. + * + * @param toolSearch + * the tool-search config + * @return this config for method chaining + */ + public ResumeSessionConfig setToolSearch(ToolSearchConfig toolSearch) { + this.toolSearch = toolSearch; + return this; + } + /** * Gets the configuration for session memory. * @@ -1745,6 +1768,36 @@ public ResumeSessionConfig setExpAssignments(JsonNode expAssignments) { return this; } + /** + * Gets whether the runtime self-fetches enterprise managed settings at session + * bootstrap on resume. + * + * @return an {@link java.util.Optional} containing {@code true} to opt into + * self-fetching managed settings, or {@link java.util.Optional#empty()} + * to use the default behavior + */ + @JsonIgnore + public Optional getEnableManagedSettings() { + return Optional.ofNullable(enableManagedSettings); + } + + /** + * Opts the runtime into self-fetching enterprise managed settings on resume. + *

+ * See {@link SessionConfig#setEnableManagedSettings(boolean)} for details. + * Re-supply on resume so the runtime re-applies the managed-settings self-fetch + * after a CLI process restart. Serialized on the wire as + * {@code enableManagedSettings}. + * + * @param enableManagedSettings + * {@code true} to opt into self-fetching managed settings + * @return this config for method chaining + */ + public ResumeSessionConfig setEnableManagedSettings(boolean enableManagedSettings) { + this.enableManagedSettings = enableManagedSettings; + return this; + } + /** * Creates a shallow clone of this {@code ResumeSessionConfig} instance. *

@@ -1806,6 +1859,7 @@ public ResumeSessionConfig clone() { : null; copy.pluginDirectories = this.pluginDirectories != null ? new ArrayList<>(this.pluginDirectories) : null; copy.largeOutput = this.largeOutput; + copy.toolSearch = this.toolSearch; copy.memory = this.memory; copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; copy.infiniteSessions = this.infiniteSessions; @@ -1819,6 +1873,7 @@ public ResumeSessionConfig clone() { copy.gitHubToken = this.gitHubToken; copy.remoteSession = this.remoteSession; copy.expAssignments = this.expAssignments; + copy.enableManagedSettings = this.enableManagedSettings; return copy; } } diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index 4917f1d8cc..ab848118e6 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -178,6 +178,9 @@ public final class ResumeSessionRequest { @JsonProperty("largeOutput") private LargeToolOutputConfig largeOutput; + @JsonProperty("toolSearch") + private ToolSearchConfig toolSearch; + @JsonProperty("memory") private MemoryConfiguration memory; @@ -214,6 +217,10 @@ public final class ResumeSessionRequest { @JsonProperty("expAssignments") private JsonNode expAssignments; + @JsonProperty("enableManagedSettings") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean enableManagedSettings; + /** Gets the session ID. @return the session ID */ public String getSessionId() { return sessionId; @@ -836,6 +843,16 @@ public void setLargeOutput(LargeToolOutputConfig largeOutput) { this.largeOutput = largeOutput; } + /** Gets tool-search config. @return the tool-search config */ + public ToolSearchConfig getToolSearch() { + return toolSearch; + } + + /** Sets tool-search config. @param toolSearch the tool-search config */ + public void setToolSearch(ToolSearchConfig toolSearch) { + this.toolSearch = toolSearch; + } + /** Gets memory config. @return the memory config */ public MemoryConfiguration getMemory() { return memory; @@ -981,4 +998,27 @@ public JsonNode getExpAssignments() { public void setExpAssignments(JsonNode expAssignments) { this.expAssignments = expAssignments; } + + /** + * Gets the self-fetch managed settings flag. @return the flag, or {@code null} + * if not set + */ + public Boolean getEnableManagedSettings() { + return enableManagedSettings; + } + + /** + * Sets the self-fetch managed settings flag. @param enableManagedSettings the + * flag + */ + public void setEnableManagedSettings(boolean enableManagedSettings) { + this.enableManagedSettings = enableManagedSettings; + } + + /** + * Clears the enableManagedSettings setting, reverting to the default behavior. + */ + public void clearEnableManagedSettings() { + this.enableManagedSettings = null; + } } diff --git a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java index cc27386c0b..e611f66c89 100644 --- a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -80,6 +80,7 @@ public class SessionConfig { private List instructionDirectories; private List pluginDirectories; private LargeToolOutputConfig largeOutput; + private ToolSearchConfig toolSearch; private MemoryConfiguration memory; private List disabledSkills; private String configDirectory; @@ -103,6 +104,7 @@ public class SessionConfig { private String remoteSession; private CloudSessionOptions cloud; private JsonNode expAssignments; + private Boolean enableManagedSettings; /** * Gets the custom session ID. @@ -1129,6 +1131,28 @@ public SessionConfig setLargeOutput(LargeToolOutputConfig largeOutput) { return this; } + /** + * Gets the tool-search override configuration. + * + * @return the tool-search config, or {@code null} for the runtime default + */ + public ToolSearchConfig getToolSearch() { + return toolSearch; + } + + /** + * Sets the tool-search override configuration. When {@code null}, the runtime + * default tool-search behavior applies. + * + * @param toolSearch + * the tool-search config + * @return this config instance for method chaining + */ + public SessionConfig setToolSearch(ToolSearchConfig toolSearch) { + this.toolSearch = toolSearch; + return this; + } + /** * Gets the configuration for session memory. * @@ -1876,6 +1900,38 @@ public SessionConfig setExpAssignments(JsonNode expAssignments) { return this; } + /** + * Gets whether the runtime self-fetches enterprise managed settings at session + * bootstrap. + * + * @return an {@link java.util.Optional} containing {@code true} to opt into + * self-fetching managed settings, or {@link java.util.Optional#empty()} + * to use the default behavior + */ + @JsonIgnore + public Optional getEnableManagedSettings() { + return Optional.ofNullable(enableManagedSettings); + } + + /** + * Opts the runtime into self-fetching enterprise managed settings + * (bypass-permissions policy) at session bootstrap. + *

+ * When {@code true}, the runtime self-fetches enterprise managed settings using + * the session's {@link #getGitHubToken() gitHubToken}. Requires + * {@code gitHubToken} to be set; if omitted, the runtime is expected to reject + * session creation (fail-closed). When unset, behaves exactly as before. + * Serialized on the wire as {@code enableManagedSettings}. + * + * @param enableManagedSettings + * {@code true} to opt into self-fetching managed settings + * @return this config instance for method chaining + */ + public SessionConfig setEnableManagedSettings(boolean enableManagedSettings) { + this.enableManagedSettings = enableManagedSettings; + return this; + } + /** * Creates a shallow clone of this {@code SessionConfig} instance. *

@@ -1931,6 +1987,7 @@ public SessionConfig clone() { : null; copy.pluginDirectories = this.pluginDirectories != null ? new ArrayList<>(this.pluginDirectories) : null; copy.largeOutput = this.largeOutput; + copy.toolSearch = this.toolSearch; copy.memory = this.memory; copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; copy.configDirectory = this.configDirectory; @@ -1955,6 +2012,7 @@ public SessionConfig clone() { copy.remoteSession = this.remoteSession; copy.cloud = this.cloud; copy.expAssignments = this.expAssignments; + copy.enableManagedSettings = this.enableManagedSettings; return copy; } } diff --git a/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java b/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java index 8a336c749a..ccf0ef5309 100644 --- a/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java +++ b/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java @@ -75,6 +75,9 @@ * controls whether the tool may be deferred (loaded lazily via tool * search) rather than always pre-loaded; {@code null} lets the * runtime decide + * @param metadata + * opaque, host-defined metadata; keys are namespaced and not part of + * the stable public API; {@code null} when unset * @see SessionConfig#setTools(java.util.List) * @see ToolHandler * @since 1.0.0 @@ -83,7 +86,36 @@ public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("description") String description, @JsonProperty("parameters") Object parameters, @JsonIgnore ToolHandler handler, @JsonProperty("overridesBuiltInTool") Boolean overridesBuiltInTool, - @JsonProperty("skipPermission") Boolean skipPermission, @JsonProperty("defer") ToolDefer defer) { + @JsonProperty("skipPermission") Boolean skipPermission, @JsonProperty("defer") ToolDefer defer, + @JsonProperty("metadata") Map metadata) { + + /** + * Creates a tool definition without a {@code metadata} bag. + *

+ * Convenience overload equivalent to the canonical constructor with + * {@code metadata} set to {@code null}. + * + * @param name + * the unique name of the tool + * @param description + * a description of what the tool does + * @param parameters + * the JSON Schema for the tool's parameters + * @param handler + * the handler function to execute when invoked + * @param overridesBuiltInTool + * whether this tool overrides a built-in tool; {@code null} for the + * default + * @param skipPermission + * whether the tool may run without a permission check; {@code null} + * for the default + * @param defer + * the deferral mode; {@code null} lets the runtime decide + */ + public ToolDefinition(String name, String description, Object parameters, ToolHandler handler, + Boolean overridesBuiltInTool, Boolean skipPermission, ToolDefer defer) { + this(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer, null); + } /** * Creates a tool definition with a JSON schema for parameters. @@ -103,7 +135,7 @@ public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("d */ public static ToolDefinition create(String name, String description, Map schema, ToolHandler handler) { - return new ToolDefinition(name, description, schema, handler, null, null, null); + return new ToolDefinition(name, description, schema, handler, null, null, null, null); } /** @@ -127,7 +159,7 @@ public static ToolDefinition create(String name, String description, Map schema, ToolHandler handler) { - return new ToolDefinition(name, description, schema, handler, true, null, null); + return new ToolDefinition(name, description, schema, handler, true, null, null, null); } /** @@ -150,7 +182,7 @@ public static ToolDefinition createOverride(String name, String description, Map */ public static ToolDefinition createSkipPermission(String name, String description, Map schema, ToolHandler handler) { - return new ToolDefinition(name, description, schema, handler, null, true, null); + return new ToolDefinition(name, description, schema, handler, null, true, null, null); } /** @@ -176,7 +208,32 @@ public static ToolDefinition createSkipPermission(String name, String descriptio */ public static ToolDefinition createWithDefer(String name, String description, Map schema, ToolHandler handler, ToolDefer defer) { - return new ToolDefinition(name, description, schema, handler, null, null, defer); + return new ToolDefinition(name, description, schema, handler, null, null, defer, null); + } + + /** + * Creates a tool definition with opaque, host-defined metadata. + *

+ * Use this factory method to attach namespaced metadata to the tool. The keys + * are not part of the stable public API; specific keys may be recognized to + * inform host-specific behavior. + * + * @param name + * the unique name of the tool + * @param description + * a description of what the tool does + * @param schema + * the JSON Schema as a {@code Map} + * @param handler + * the handler function to execute when invoked + * @param metadata + * the opaque metadata map + * @return a new tool definition with the metadata set + * @since 1.0.7 + */ + public static ToolDefinition createWithMetadata(String name, String description, Map schema, + ToolHandler handler, Map metadata) { + return new ToolDefinition(name, description, schema, handler, null, null, null, metadata); } /** @@ -247,7 +304,7 @@ public static List fromClass(Class clazz) { */ @CopilotExperimental public ToolDefinition overridesBuiltInTool(boolean value) { - return new ToolDefinition(name, description, parameters, handler, value, skipPermission, defer); + return new ToolDefinition(name, description, parameters, handler, value, skipPermission, defer, metadata); } /** @@ -261,7 +318,7 @@ public ToolDefinition overridesBuiltInTool(boolean value) { */ @CopilotExperimental public ToolDefinition skipPermission(boolean value) { - return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, value, defer); + return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, value, defer, metadata); } /** @@ -275,7 +332,23 @@ public ToolDefinition skipPermission(boolean value) { */ @CopilotExperimental public ToolDefinition defer(ToolDefer value) { - return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, value); + return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, value, + metadata); + } + + /** + * Returns a copy with the opaque {@code metadata} bag set. + * + * @param value + * the opaque, host-defined metadata; keys are namespaced and not + * part of the stable public API + * @return a new {@code ToolDefinition} with the metadata applied + * @since 1.0.7 + */ + @CopilotExperimental + public ToolDefinition metadata(Map value) { + return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer, + value); } // ------------------------------------------------------------------ @@ -319,7 +392,7 @@ public static ToolDefinition from(String name, String description, Supplier< R result = handler.get(); return CompletableFuture.completedFuture(formatResult(result, mapper)); }; - return new ToolDefinition(name, description, schema, toolHandler, null, null, null); + return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null); } /** @@ -361,7 +434,7 @@ public static ToolDefinition from(String name, String description, Param R result = handler.apply(arg1); return CompletableFuture.completedFuture(formatResult(result, mapper)); }; - return new ToolDefinition(name, description, schema, toolHandler, null, null, null); + return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null); } /** @@ -409,7 +482,7 @@ public static ToolDefinition from(String name, String description, P R result = handler.apply(arg1, arg2); return CompletableFuture.completedFuture(formatResult(result, mapper)); }; - return new ToolDefinition(name, description, schema, toolHandler, null, null, null); + return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null); } // ------------------------------------------------------------------ @@ -458,7 +531,7 @@ public static ToolDefinition fromAsync(String name, String description, } return future.thenApply(result -> formatResult(result, mapper)); }; - return new ToolDefinition(name, description, schema, toolHandler, null, null, null); + return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null); } /** @@ -506,7 +579,7 @@ public static ToolDefinition fromAsync(String name, String description, } return future.thenApply(result -> formatResult(result, mapper)); }; - return new ToolDefinition(name, description, schema, toolHandler, null, null, null); + return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null); } /** @@ -551,7 +624,7 @@ public static ToolDefinition fromAsync(String name, String descripti } return future.thenApply(result -> formatResult(result, mapper)); }; - return new ToolDefinition(name, description, schema, toolHandler, null, null, null); + return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null); } // ------------------------------------------------------------------ @@ -594,7 +667,7 @@ public static ToolDefinition fromWithToolInvocation(String name, String desc R result = handler.apply(invocation); return CompletableFuture.completedFuture(formatResult(result, mapper)); }; - return new ToolDefinition(name, description, schema, toolHandler, null, null, null); + return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null); } /** @@ -640,7 +713,7 @@ public static ToolDefinition fromWithToolInvocation(String name, String R result = handler.apply(arg1, invocation); return CompletableFuture.completedFuture(formatResult(result, mapper)); }; - return new ToolDefinition(name, description, schema, toolHandler, null, null, null); + return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null); } // ------------------------------------------------------------------ @@ -689,7 +762,7 @@ public static ToolDefinition fromAsyncWithToolInvocation(String name, String } return future.thenApply(result -> formatResult(result, mapper)); }; - return new ToolDefinition(name, description, schema, toolHandler, null, null, null); + return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null); } /** @@ -741,7 +814,7 @@ public static ToolDefinition fromAsyncWithToolInvocation(String name, St } return future.thenApply(result -> formatResult(result, mapper)); }; - return new ToolDefinition(name, description, schema, toolHandler, null, null, null); + return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null); } // ------------------------------------------------------------------ diff --git a/java/src/main/java/com/github/copilot/rpc/ToolInvocation.java b/java/src/main/java/com/github/copilot/rpc/ToolInvocation.java index 048fede64a..efe24fd6ae 100644 --- a/java/src/main/java/com/github/copilot/rpc/ToolInvocation.java +++ b/java/src/main/java/com/github/copilot/rpc/ToolInvocation.java @@ -4,6 +4,7 @@ package com.github.copilot.rpc; +import java.util.List; import java.util.Map; import com.fasterxml.jackson.annotation.JsonInclude; @@ -11,6 +12,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.generated.rpc.CurrentToolMetadata; /** * Represents a tool invocation request from the AI assistant. @@ -40,6 +42,7 @@ public final class ToolInvocation { private String toolCallId; private String toolName; private JsonNode argumentsNode; + private List availableTools; /** * Gets the session ID where the tool was invoked. @@ -174,4 +177,35 @@ public ToolInvocation setArguments(JsonNode arguments) { this.argumentsNode = arguments; return this; } + + /** + * Gets a snapshot of the session's currently initialized tools. + *

+ * The SDK populates this only when the invocation targets the built-in + * tool-search tool ({@code "tool_search_tool"}), so a tool-search override can + * rank or filter the live catalog — including MCP tools configured in settings + * — without issuing its own RPC. It is {@code null} for every other tool + * invocation. + * + * @return the available tools snapshot, or {@code null} if not applicable + * @since 1.0.7 + */ + public List getAvailableTools() { + return availableTools; + } + + /** + * Sets the available tools snapshot. + *

+ * Note: This method is intended for internal SDK use. Users + * typically do not need to call this method directly. + * + * @param availableTools + * the available tools snapshot + * @return this invocation for method chaining + */ + public ToolInvocation setAvailableTools(List availableTools) { + this.availableTools = availableTools; + return this; + } } diff --git a/java/src/main/java/com/github/copilot/rpc/ToolResultObject.java b/java/src/main/java/com/github/copilot/rpc/ToolResultObject.java index e55ff9ab6e..2e101acbd7 100644 --- a/java/src/main/java/com/github/copilot/rpc/ToolResultObject.java +++ b/java/src/main/java/com/github/copilot/rpc/ToolResultObject.java @@ -31,7 +31,7 @@ *

Example: Custom Result

* *
{@code
- * return new ToolResultObject("success", "Result text", null, null, null, null);
+ * return new ToolResultObject("success", "Result text", null, null, null, null, null);
  * }
* * @param resultType @@ -46,6 +46,8 @@ * the session log text * @param toolTelemetry * the tool telemetry data + * @param toolReferences + * names of tools returned by a tool-search tool * @see ToolHandler * @see ToolBinaryResult * @since 1.0.0 @@ -55,7 +57,33 @@ public record ToolResultObject(@JsonProperty("resultType") String resultType, @JsonProperty("textResultForLlm") String textResultForLlm, @JsonProperty("binaryResultsForLlm") List binaryResultsForLlm, @JsonProperty("error") String error, @JsonProperty("sessionLog") String sessionLog, - @JsonProperty("toolTelemetry") Map toolTelemetry) { + @JsonProperty("toolTelemetry") Map toolTelemetry, + @JsonProperty("toolReferences") List toolReferences) { + + /** + * Creates a result without tool references. + *

+ * Provided for source and binary compatibility with callers written or compiled + * before the {@code toolReferences} component was added. Delegates to the + * canonical constructor with {@code toolReferences} set to {@code null}. + * + * @param resultType + * the result type ("success" or "error"), defaults to "success" + * @param textResultForLlm + * the text result to be sent to the LLM + * @param binaryResultsForLlm + * the list of binary results to be sent to the LLM + * @param error + * the error message, or {@code null} if successful + * @param sessionLog + * the session log text + * @param toolTelemetry + * the tool telemetry data + */ + public ToolResultObject(String resultType, String textResultForLlm, List binaryResultsForLlm, + String error, String sessionLog, Map toolTelemetry) { + this(resultType, textResultForLlm, binaryResultsForLlm, error, sessionLog, toolTelemetry, null); + } /** * Creates a success result with the given text. @@ -65,7 +93,7 @@ public record ToolResultObject(@JsonProperty("resultType") String resultType, * @return a success result */ public static ToolResultObject success(String textResultForLlm) { - return new ToolResultObject("success", textResultForLlm, null, null, null, null); + return new ToolResultObject("success", textResultForLlm, null, null, null, null, null); } /** @@ -76,7 +104,7 @@ public static ToolResultObject success(String textResultForLlm) { * @return an error result */ public static ToolResultObject error(String error) { - return new ToolResultObject("error", null, null, error, null, null); + return new ToolResultObject("error", null, null, error, null, null, null); } /** @@ -89,7 +117,7 @@ public static ToolResultObject error(String error) { * @return an error result */ public static ToolResultObject error(String textResultForLlm, String error) { - return new ToolResultObject("error", textResultForLlm, null, error, null, null); + return new ToolResultObject("error", textResultForLlm, null, error, null, null, null); } /** @@ -106,6 +134,6 @@ public static ToolResultObject error(String textResultForLlm, String error) { * @return a failure result */ public static ToolResultObject failure(String textResultForLlm, String error) { - return new ToolResultObject("failure", textResultForLlm, null, error, null, null); + return new ToolResultObject("failure", textResultForLlm, null, error, null, null, null); } } diff --git a/java/src/main/java/com/github/copilot/rpc/ToolSearchConfig.java b/java/src/main/java/com/github/copilot/rpc/ToolSearchConfig.java new file mode 100644 index 0000000000..dd27ddf868 --- /dev/null +++ b/java/src/main/java/com/github/copilot/rpc/ToolSearchConfig.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Overrides the runtime's built-in tool-search behavior. + *

+ * Tool search defers tools to keep the model's active tool set small. To + * override the tool-search tool's implementation, register a tool named + * {@code "tool_search_tool"} with {@code overridesBuiltInTool} set to + * {@code true}. + * + * @since 1.3.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ToolSearchConfig { + + @JsonProperty("enabled") + private Boolean enabled; + + @JsonProperty("deferThreshold") + private Integer deferThreshold; + + /** + * Gets whether tool search is enabled. + * + * @return {@code true} if enabled, {@code false} if disabled, or {@code null} + * for the runtime default + */ + public Boolean getEnabled() { + return enabled; + } + + /** + * Toggle that enables or disables tool search. + * + * @param enabled + * {@code true} to enable, {@code false} to disable + * @return this config for method chaining + */ + public ToolSearchConfig setEnabled(Boolean enabled) { + this.enabled = enabled; + return this; + } + + /** + * Gets the tool count above which MCP and external tools are deferred behind + * tool search. + * + * @return the defer threshold, or {@code null} for the runtime default (30) + */ + public Integer getDeferThreshold() { + return deferThreshold; + } + + /** + * Sets the tool count above which MCP and external tools are deferred behind + * tool search. Defaults to the runtime default (30) when unset. + * + * @param deferThreshold + * the threshold value + * @return this config for method chaining + */ + public ToolSearchConfig setDeferThreshold(Integer deferThreshold) { + this.deferThreshold = deferThreshold; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/rpc/package-info.java b/java/src/main/java/com/github/copilot/rpc/package-info.java index edc7dedcfc..83772cd048 100644 --- a/java/src/main/java/com/github/copilot/rpc/package-info.java +++ b/java/src/main/java/com/github/copilot/rpc/package-info.java @@ -80,7 +80,7 @@ *

Usage Example

* *
{@code
- * var config = new SessionConfig().setModel("gpt-4.1").setStreaming(true)
+ * var config = new SessionConfig().setModel("gpt-5.4").setStreaming(true)
  * 		.setSystemMessage(new SystemMessageConfig().setMode(SystemMessageMode.APPEND)
  * 				.setContent("Be concise in your responses."))
  * 		.setTools(List.of(ToolDefinition.create("my_tool", "Description", schema, handler)));
diff --git a/java/src/main/java/com/github/copilot/tool/CopilotTool.java b/java/src/main/java/com/github/copilot/tool/CopilotTool.java
index 0bad327b99..db9e3ca62d 100644
--- a/java/src/main/java/com/github/copilot/tool/CopilotTool.java
+++ b/java/src/main/java/com/github/copilot/tool/CopilotTool.java
@@ -50,4 +50,87 @@
 
     /** Defer configuration for this tool. */
     ToolDefer defer() default ToolDefer.NONE;
+
+    /**
+     * Opaque, host-defined metadata for this tool. Keys are namespaced and not part
+     * of the stable public API; specific keys may be recognized to inform
+     * host-specific behavior.
+     *
+     * 

+ * Because annotation members cannot express arbitrary maps, this uses a + * deliberately shallow representation: each {@link MetadataEntry} maps a string + * key to a single {@link MetadataValue} that is either a boolean, a string, or + * a one-level map of named boolean {@link MetadataFlag flags}. Numbers, arrays, + * and deeper nesting are not supported here; use the programmatic + * {@code ToolDefinition.createWithMetadata(...)} / + * {@code ToolDefinition.metadata(...)} API for richer values. + * + *

+ * Example emitted shape: + * + *

+     * Map.of("github.com/copilot:safeForTelemetry", Map.of("name", true, "inputsNames", false))
+     * 
+ */ + MetadataEntry[] metadata() default {}; + + /** + * A single metadata key/value pair. Used only as a member value of + * {@link CopilotTool#metadata()}. + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({}) + @interface MetadataEntry { + + /** The namespaced metadata key. */ + String key(); + + /** The value associated with {@link #key()}. */ + MetadataValue value(); + } + + /** + * A metadata value. Exactly one representation is intended per value: a map of + * named boolean {@link #flags()} (when non-empty), otherwise a {@link #str()} + * (when non-empty), otherwise a {@link #bool()}. + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({}) + @interface MetadataValue { + + /** + * Scalar boolean value. Used when {@link #flags()} and {@link #str()} are + * unset. + */ + boolean bool() default false; + + /** + * Scalar string value. Used when {@link #flags()} is empty and this is + * non-empty. + */ + String str() default ""; + + /** + * Object-like value: a one-level map of named boolean flags. Takes precedence + * when non-empty. + */ + MetadataFlag[] flags() default {}; + } + + /** + * A single named boolean flag within a {@link MetadataValue#flags()} map. + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({}) + @interface MetadataFlag { + + /** The flag name (map key). */ + String name(); + + /** The flag value. */ + boolean value(); + } } diff --git a/java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java b/java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java index 9bf4e99c03..03af4a7cd9 100644 --- a/java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java +++ b/java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java @@ -276,10 +276,48 @@ private void writeToolDefinition(PrintWriter out, ExecutableElement method) { out.println(" },"); out.println(" " + overridesArg + ","); out.println(" " + skipPermArg + ","); - out.println(" " + deferArg); + out.println(" " + deferArg + ","); + out.println(" " + metadataSource(annotation)); out.print(" )"); } + /** + * Converts the {@code @CopilotTool(metadata = ...)} entries into a Java source + * literal. Returns {@code "null"} when no metadata is present, otherwise a + * {@code Map.of(...)} expression. + */ + private String metadataSource(CopilotTool annotation) { + CopilotTool.MetadataEntry[] entries = annotation.metadata(); + if (entries.length == 0) { + return "null"; + } + List parts = new ArrayList<>(); + for (CopilotTool.MetadataEntry entry : entries) { + parts.add("\"" + escapeJava(entry.key()) + "\", " + metadataValueSource(entry.value())); + } + return "Map.of(" + String.join(", ", parts) + ")"; + } + + /** + * Converts a single {@link CopilotTool.MetadataValue} into a Java source + * literal. A non-empty {@code flags} map takes precedence, then a non-empty + * {@code str}, otherwise the {@code bool} scalar. + */ + private String metadataValueSource(CopilotTool.MetadataValue value) { + CopilotTool.MetadataFlag[] flags = value.flags(); + if (flags.length > 0) { + List flagParts = new ArrayList<>(); + for (CopilotTool.MetadataFlag flag : flags) { + flagParts.add("\"" + escapeJava(flag.name()) + "\", " + flag.value()); + } + return "Map.of(" + String.join(", ", flagParts) + ")"; + } + if (!value.str().isEmpty()) { + return "\"" + escapeJava(value.str()) + "\""; + } + return String.valueOf(value.bool()); + } + private String generateSchemaWithParamMetadata(List parameters) { List schemaParameters = getSchemaParameters(parameters); diff --git a/java/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java b/java/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java index daf524945e..3025c64c39 100644 --- a/java/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java +++ b/java/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java @@ -10,6 +10,7 @@ import static com.github.copilot.CopilotRequestTestSupport.setupCapiAuth; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -68,6 +69,7 @@ void threadsSessionIdForCapiAndByok() throws Exception { assertFalse(capiInference.isEmpty(), "Expected at least one intercepted inference request"); for (InterceptedRequest r : capiInference) { assertEquals(capiSessionId, r.sessionId(), "CAPI inference request must carry the session id"); + assertAgentMetadata(r); } assertTrue(assistantText(capiResult).contains("OK from the synthetic"), "Expected synthetic content in CAPI assistant reply, got " + assistantText(capiResult)); @@ -91,10 +93,18 @@ void threadsSessionIdForCapiAndByok() throws Exception { assertTrue(byokInference.size() > before, "Expected at least one intercepted BYOK inference request"); for (InterceptedRequest r : byokInference.subList(before, byokInference.size())) { assertEquals(byokSessionId, r.sessionId(), "BYOK inference request must carry the session id"); + assertAgentMetadata(r); } assertNotEquals(capiSessionId, byokSessionId, "Expected per-session ids to differ between turns"); assertTrue(assistantText(byokResult).contains("OK from the synthetic"), "Expected synthetic content in BYOK assistant reply, got " + assistantText(byokResult)); } } + + private static void assertAgentMetadata(InterceptedRequest request) { + assertNotNull(request.agentId(), "Inference request must carry an agent id"); + assertFalse(request.agentId().isEmpty(), "Inference request must carry an agent id"); + assertNotNull(request.interactionType(), "Inference request must carry an interaction type"); + assertFalse(request.interactionType().isEmpty(), "Inference request must carry an interaction type"); + } } diff --git a/java/src/test/java/com/github/copilot/CopilotRequestTestSupport.java b/java/src/test/java/com/github/copilot/CopilotRequestTestSupport.java index 7a2e7f2f0f..ecbf92068f 100644 --- a/java/src/test/java/com/github/copilot/CopilotRequestTestSupport.java +++ b/java/src/test/java/com/github/copilot/CopilotRequestTestSupport.java @@ -473,7 +473,8 @@ static String assistantText(AssistantMessageEvent event) { } /** A single request the handler intercepted. */ - record InterceptedRequest(String url, String sessionId, String body) { + record InterceptedRequest(String url, String sessionId, String agentId, String parentAgentId, + String interactionType, String body) { } /** @@ -509,7 +510,8 @@ protected HttpResponse sendRequest(HttpRequest request, CopilotRequ throws Exception { String url = request.uri().toString(); String body = requestBodyText(request); - records.add(new InterceptedRequest(url, ctx.sessionId(), body)); + records.add(new InterceptedRequest(url, ctx.sessionId(), ctx.agentId(), ctx.parentAgentId(), + ctx.interactionType(), body)); if (isInferenceUrl(url)) { return buildInferenceResponse(url, body, text); } diff --git a/java/src/test/java/com/github/copilot/DataObjectCoverageTest.java b/java/src/test/java/com/github/copilot/DataObjectCoverageTest.java index ece824234b..f38a038368 100644 --- a/java/src/test/java/com/github/copilot/DataObjectCoverageTest.java +++ b/java/src/test/java/com/github/copilot/DataObjectCoverageTest.java @@ -186,7 +186,7 @@ void postToolUseHookInputSessionIdRoundTrip() { assertEquals("session-xyz", input.getSessionId()); } - // ===== CustomAgentConfig model field ===== + // ===== CustomAgentConfig model fields ===== @Test void customAgentConfigModelGetterAndSetter() { @@ -227,6 +227,36 @@ void customAgentConfigModelOmittedWhenNull() throws Exception { assertFalse(json.contains("\"model\"")); } + @Test + void customAgentConfigReasoningEffortGetterAndFluentSetter() { + var cfg = new CustomAgentConfig(); + assertNull(cfg.getReasoningEffort()); + + var result = cfg.setReasoningEffort("high"); + assertSame(cfg, result); + assertEquals("high", cfg.getReasoningEffort()); + } + + @Test + void customAgentConfigReasoningEffortSerializationRoundTrip() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var cfg = new CustomAgentConfig().setName("reasoning-agent").setReasoningEffort("high"); + + var json = mapper.writeValueAsString(cfg); + assertTrue(json.contains("\"reasoningEffort\":\"high\"")); + + var deserialized = mapper.readValue(json, CustomAgentConfig.class); + assertEquals("high", deserialized.getReasoningEffort()); + } + + @Test + void customAgentConfigReasoningEffortOmittedWhenNull() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(new CustomAgentConfig().setName("default-agent")); + + assertFalse(json.contains("\"reasoningEffort\"")); + } + // ===== PermissionRequestResult setRules ===== @Test diff --git a/java/src/test/java/com/github/copilot/ForwardCompatibilityTest.java b/java/src/test/java/com/github/copilot/ForwardCompatibilityTest.java index 40166307e3..9163ae1357 100644 --- a/java/src/test/java/com/github/copilot/ForwardCompatibilityTest.java +++ b/java/src/test/java/com/github/copilot/ForwardCompatibilityTest.java @@ -56,6 +56,22 @@ void parse_unknownEventType_returnsUnknownSessionEvent() throws Exception { assertEquals("future.feature_from_server", result.getType()); } + @Test + void parse_internalEventType_returnsUnknownSessionEvent() throws Exception { + String json = """ + { + "id": "12345678-1234-1234-1234-123456789abc", + "timestamp": "2026-06-15T10:30:00Z", + "type": "session.memory_changed", + "data": {} + } + """; + SessionEvent result = MAPPER.readValue(json, SessionEvent.class); + + assertInstanceOf(UnknownSessionEvent.class, result); + assertEquals("session.memory_changed", result.getType()); + } + @Test void parse_unknownEventType_preservesOriginalType() throws Exception { String json = """ diff --git a/java/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java b/java/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java index 0d323183b5..83365455e7 100644 --- a/java/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java +++ b/java/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)) + .switchTo(new SessionModelSwitchToParams(null, selectionId, null, null, null, null, null)) .get(30, TimeUnit.SECONDS); var current = session.getRpc().model.getCurrent().get(30, TimeUnit.SECONDS); assertEquals(selectionId, current.modelId()); diff --git a/java/src/test/java/com/github/copilot/RpcWrappersTest.java b/java/src/test/java/com/github/copilot/RpcWrappersTest.java index 9a3559d66d..7493c6e470 100644 --- a/java/src/test/java/com/github/copilot/RpcWrappersTest.java +++ b/java/src/test/java/com/github/copilot/RpcWrappersTest.java @@ -205,7 +205,7 @@ void sessionRpc_model_switchTo_merges_sessionId_with_extra_params() { var session = new SessionRpc(stub, "sess-xyz"); // switchTo takes extra params beyond sessionId - var switchParams = new SessionModelSwitchToParams(null, "gpt-5", null, null, null, null); + var switchParams = new SessionModelSwitchToParams(null, "gpt-5", null, null, null, null, null); session.model.switchTo(switchParams); assertEquals(1, stub.calls.size()); diff --git a/java/src/test/java/com/github/copilot/SessionCanvasSnapshotTest.java b/java/src/test/java/com/github/copilot/SessionCanvasSnapshotTest.java index 00db1d01c3..f50138b3b4 100644 --- a/java/src/test/java/com/github/copilot/SessionCanvasSnapshotTest.java +++ b/java/src/test/java/com/github/copilot/SessionCanvasSnapshotTest.java @@ -118,7 +118,7 @@ void getOpenCanvasesReturnsImmutableCopy() { var canvases = session.getOpenCanvases(); assertThrows(UnsupportedOperationException.class, - () -> canvases.add(new OpenCanvasInstance("x", "ext", null, "c", null, null, null, null))); + () -> canvases.add(new OpenCanvasInstance("x", "ext", null, "c", null, null, null, null, null))); // The returned list is a point-in-time snapshot, not a live view: a // subsequent event must not change the previously-returned list. @@ -133,9 +133,9 @@ void getOpenCanvasesReturnsImmutableCopy() { @Test void setOpenCanvasesSeedsAndFiltersNulls() { var seed = new java.util.ArrayList(); - seed.add(new OpenCanvasInstance("inst-1", "ext", null, "canvas-a", null, null, null, null)); + seed.add(new OpenCanvasInstance("inst-1", "ext", null, "canvas-a", null, null, null, null, null)); seed.add(null); - seed.add(new OpenCanvasInstance("inst-2", "ext", null, "canvas-b", null, null, null, null)); + seed.add(new OpenCanvasInstance("inst-2", "ext", null, "canvas-b", null, null, null, null, null)); session.setOpenCanvases(seed); @@ -195,8 +195,8 @@ void resumeSessionResponseDeserializesOpenCanvases() throws Exception { private static SessionCanvasOpenedEvent openedEvent(String instanceId, String canvasId) { var event = new SessionCanvasOpenedEvent(); - event.setData(new SessionCanvasOpenedEventData(instanceId, "ext-id", "Ext Name", canvasId, "Title", "ok", null, - null)); + event.setData(new SessionCanvasOpenedEventData(instanceId, "ext-id", "Ext Name", canvasId, null, "Title", "ok", + null, null)); return event; } diff --git a/java/src/test/java/com/github/copilot/SessionEventHandlingTest.java b/java/src/test/java/com/github/copilot/SessionEventHandlingTest.java index 2e54abf9d1..1cf3ceff1f 100644 --- a/java/src/test/java/com/github/copilot/SessionEventHandlingTest.java +++ b/java/src/test/java/com/github/copilot/SessionEventHandlingTest.java @@ -180,7 +180,7 @@ void testHandlerReceivesCorrectEventData() { SessionStartEvent startEvent = createSessionStartEvent(); startEvent.setData(new SessionStartEvent.SessionStartEventData("my-session-123", null, null, null, null, null, - null, null, null, null, null, null, null, null)); + null, null, null, null, null, null, null, null, null)); dispatchEvent(startEvent); AssistantMessageEvent msgEvent = createAssistantMessageEvent("Test content"); @@ -857,7 +857,7 @@ private SessionStartEvent createSessionStartEvent() { private SessionStartEvent createSessionStartEvent(String sessionId) { var event = new SessionStartEvent(); var data = new SessionStartEvent.SessionStartEventData(sessionId, null, null, null, null, null, null, null, - null, null, null, null, null, null); + null, null, null, null, null, null, null); event.setData(data); return event; } @@ -865,7 +865,7 @@ private SessionStartEvent createSessionStartEvent(String sessionId) { private AssistantMessageEvent createAssistantMessageEvent(String content) { var event = new AssistantMessageEvent(); var data = new AssistantMessageEvent.AssistantMessageEventData(null, null, content, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null, null, null, null); event.setData(data); return event; } diff --git a/java/src/test/java/com/github/copilot/SubagentHooksE2ETest.java b/java/src/test/java/com/github/copilot/SubagentHooksE2ETest.java new file mode 100644 index 0000000000..c2ad45ff24 --- /dev/null +++ b/java/src/test/java/com/github/copilot/SubagentHooksE2ETest.java @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.InputStream; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.rpc.CopilotClientOptions; +import com.github.copilot.rpc.MessageOptions; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.PostToolUseHookOutput; +import com.github.copilot.rpc.PreToolUseHookOutput; +import com.github.copilot.rpc.SessionConfig; +import com.github.copilot.rpc.SessionHooks; + +public class SubagentHooksE2ETest { + + private static final String SNAPSHOT_NAME = "should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls"; + + @Test + void shouldInvokePreToolUseAndPostToolUseHooksForSubAgentToolCalls() throws Exception { + try (E2ETestContext ctx = E2ETestContext.create()) { + ctx.configureForTest("subagent_hooks", SNAPSHOT_NAME); + + ConcurrentLinkedQueue hookLog = new ConcurrentLinkedQueue<>(); + RecordingForwardingRequestHandler requestHandler = new RecordingForwardingRequestHandler(); + HashMap env = new HashMap<>(ctx.getEnvironment()); + env.put("COPILOT_EXP_COPILOT_CLI_SESSION_BASED_SUBAGENTS", "true"); + + try (CopilotClient client = ctx + .createClient(new CopilotClientOptions().setEnvironment(env).setRequestHandler(requestHandler))) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> { + hookLog.add(new HookEntry("pre", input.getToolName(), input.getSessionId())); + return CompletableFuture.completedFuture(PreToolUseHookOutput.allow()); + }).setOnPostToolUse((input, invocation) -> { + hookLog.add(new HookEntry("post", input.getToolName(), input.getSessionId())); + return CompletableFuture.completedFuture((PostToolUseHookOutput) null); + }))) + .get(); + try { + Files.writeString(ctx.getWorkDir().resolve("subagent-test.txt"), "Hello from subagent test!"); + session.sendAndWait(new MessageOptions() + .setPrompt("Use the task tool to spawn an explore agent that reads the file " + + "subagent-test.txt in the current directory and reports its contents. " + + "You must use the task tool.")) + .get(120, TimeUnit.SECONDS); + + HookEntry taskPre = hookLog.stream() + .filter(h -> h.kind().equals("pre") && h.toolName().equals("task")).findFirst() + .orElse(null); + assertNotNull(taskPre, "preToolUse should fire for the parent's 'task' tool call"); + + List viewPre = hookLog.stream() + .filter(h -> h.kind().equals("pre") && h.toolName().equals("view")).toList(); + List viewPost = hookLog.stream() + .filter(h -> h.kind().equals("post") && h.toolName().equals("view")).toList(); + assertFalse(viewPre.isEmpty(), "preToolUse should fire for the sub-agent's 'view' tool call"); + assertFalse(viewPost.isEmpty(), "postToolUse should fire for the sub-agent's 'view' tool call"); + assertNotEquals(taskPre.sessionId(), viewPre.get(0).sessionId(), + "Sub-agent tool hooks should have a different sessionId than parent tool hooks"); + assertSubagentRequestMetadata(requestHandler.inferenceRequests()); + } finally { + session.close(); + } + } + } + } + + private static void assertSubagentRequestMetadata(List records) { + assertFalse(records.isEmpty(), "request handler should observe inference requests"); + RequestRecord subagentRequest = records.stream() + .filter(r -> r.parentAgentId() != null && !r.parentAgentId().isEmpty()).findFirst().orElse(null); + assertNotNull(subagentRequest, "sub-agent inference request should carry a parentAgentId"); + assertFalse(subagentRequest.agentId() == null || subagentRequest.agentId().isEmpty(), + "sub-agent inference request should carry an agentId"); + assertFalse(subagentRequest.interactionType() == null || subagentRequest.interactionType().isEmpty(), + "sub-agent inference request should carry an interactionType"); + assertNotEquals(subagentRequest.parentAgentId(), subagentRequest.agentId()); + } + + private static boolean isInferenceUrl(String url) { + String u = url.toLowerCase(); + return u.endsWith("/chat/completions") || u.endsWith("/responses") || u.endsWith("/v1/messages") + || u.endsWith("/messages"); + } + + private record HookEntry(String kind, String toolName, String sessionId) { + } + + private record RequestRecord(String url, String agentId, String parentAgentId, String interactionType) { + } + + private static final class RecordingForwardingRequestHandler extends CopilotRequestHandler { + private final ConcurrentLinkedQueue records = new ConcurrentLinkedQueue<>(); + + List inferenceRequests() { + return records.stream().filter(r -> isInferenceUrl(r.url())).toList(); + } + + @Override + protected HttpResponse sendRequest(HttpRequest request, CopilotRequestContext ctx) + throws Exception { + records.add(new RequestRecord(request.uri().toString(), ctx.agentId(), ctx.parentAgentId(), + ctx.interactionType())); + return super.sendRequest(request, ctx); + } + } +} diff --git a/java/src/test/java/com/github/copilot/ToolDefinitionTest.java b/java/src/test/java/com/github/copilot/ToolDefinitionTest.java index 614e6ab4fa..66c9f9ec86 100644 --- a/java/src/test/java/com/github/copilot/ToolDefinitionTest.java +++ b/java/src/test/java/com/github/copilot/ToolDefinitionTest.java @@ -59,4 +59,73 @@ void testDeferNeverIsSerialized() throws Exception { assertEquals("never", json.get("defer").asText()); } + + @Test + void testMetadataIsSerialized() throws Exception { + Map metadata = Map.of("github.com/copilot:safeForTelemetry", + Map.of("name", true, "inputsNames", false)); + ToolDefinition tool = ToolDefinition.createWithMetadata("my_tool", "A tool", schema(), + invocation -> CompletableFuture.completedFuture("ok"), metadata); + + ObjectNode json = (ObjectNode) MAPPER.readTree(MAPPER.writeValueAsString(tool)); + + assertTrue(json.has("metadata")); + assertTrue(json.get("metadata").has("github.com/copilot:safeForTelemetry")); + } + + @Test + void testMetadataOmittedWhenNull() throws Exception { + ToolDefinition tool = ToolDefinition.create("my_tool", "A tool", schema(), + invocation -> CompletableFuture.completedFuture("ok")); + + ObjectNode json = (ObjectNode) MAPPER.readTree(MAPPER.writeValueAsString(tool)); + + assertFalse(json.has("metadata")); + } + + @Test + void testSevenArgConstructorLeavesMetadataNull() throws Exception { + ToolDefinition tool = new ToolDefinition("my_tool", "A tool", schema(), + invocation -> CompletableFuture.completedFuture("ok"), null, null, null); + + assertNull(tool.metadata()); + + ObjectNode json = (ObjectNode) MAPPER.readTree(MAPPER.writeValueAsString(tool)); + + assertFalse(json.has("metadata")); + } + + @Test + void testMetadataCopyMethodSerializes() throws Exception { + Map metadata = Map.of("github.com/copilot:safeForTelemetry", + Map.of("name", true, "inputsNames", false)); + ToolDefinition tool = ToolDefinition + .create("my_tool", "A tool", schema(), invocation -> CompletableFuture.completedFuture("ok")) + .metadata(metadata); + + assertEquals(metadata, tool.metadata()); + + ObjectNode json = (ObjectNode) MAPPER.readTree(MAPPER.writeValueAsString(tool)); + + assertTrue(json.get("metadata").has("github.com/copilot:safeForTelemetry")); + } + + @Test + void testChainingFlagsPreservesMetadata() throws Exception { + Map metadata = Map.of("github.com/copilot:safeForTelemetry", Map.of("name", true)); + + ToolDefinition metadataFirst = ToolDefinition + .create("my_tool", "A tool", schema(), invocation -> CompletableFuture.completedFuture("ok")) + .metadata(metadata).overridesBuiltInTool(true).skipPermission(true).defer(ToolDefer.NEVER); + + ToolDefinition flagsFirst = ToolDefinition + .create("my_tool", "A tool", schema(), invocation -> CompletableFuture.completedFuture("ok")) + .overridesBuiltInTool(true).skipPermission(true).defer(ToolDefer.NEVER).metadata(metadata); + + assertEquals(metadata, metadataFirst.metadata()); + assertEquals(metadata, flagsFirst.metadata()); + assertEquals(Boolean.TRUE, flagsFirst.overridesBuiltInTool()); + assertEquals(Boolean.TRUE, flagsFirst.skipPermission()); + assertEquals(ToolDefer.NEVER, flagsFirst.defer()); + } } diff --git a/java/src/test/java/com/github/copilot/ToolResultObjectSerializationTest.java b/java/src/test/java/com/github/copilot/ToolResultObjectSerializationTest.java new file mode 100644 index 0000000000..3f08cae943 --- /dev/null +++ b/java/src/test/java/com/github/copilot/ToolResultObjectSerializationTest.java @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.rpc.ToolResultObject; + +/** + * Verifies JSON (de)serialization of the {@code toolReferences} field on + * {@link ToolResultObject}, including that it is omitted when {@code null} (via + * {@code @JsonInclude(NON_NULL)}) and preserved by the backward-compatible + * six-argument constructor. + */ +class ToolResultObjectSerializationTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void serializesToolReferences() { + var result = new ToolResultObject("success", "found 2 tools", null, null, null, null, + List.of("get_weather", "check_status")); + + JsonNode node = MAPPER.valueToTree(result); + + assertEquals("found 2 tools", node.get("textResultForLlm").asText()); + JsonNode refs = node.get("toolReferences"); + assertNotNull(refs); + assertTrue(refs.isArray()); + assertEquals(2, refs.size()); + assertEquals("get_weather", refs.get(0).asText()); + assertEquals("check_status", refs.get(1).asText()); + } + + @Test + void omitsToolReferencesWhenNull() { + JsonNode node = MAPPER.valueToTree(ToolResultObject.success("ok")); + + assertFalse(node.has("toolReferences")); + } + + @Test + void sixArgConstructorLeavesToolReferencesNull() { + var result = new ToolResultObject("success", "ok", null, null, null, null); + + assertNull(result.toolReferences()); + assertFalse(MAPPER.valueToTree(result).has("toolReferences")); + } + + @Test + void deserializesToolReferences() throws Exception { + String json = "{\"resultType\":\"success\",\"textResultForLlm\":\"x\"," + + "\"toolReferences\":[\"alpha\",\"beta\"]}"; + + ToolResultObject result = MAPPER.readValue(json, ToolResultObject.class); + + assertEquals(List.of("alpha", "beta"), result.toolReferences()); + } +} diff --git a/java/src/test/java/com/github/copilot/ToolResultsTest.java b/java/src/test/java/com/github/copilot/ToolResultsTest.java index 54216d9216..8278fdf28f 100644 --- a/java/src/test/java/com/github/copilot/ToolResultsTest.java +++ b/java/src/test/java/com/github/copilot/ToolResultsTest.java @@ -68,7 +68,7 @@ void testShouldHandleToolResultWithRejectedResultType() throws Exception { toolHandlerCalled[0] = true; return CompletableFuture.completedFuture(new ToolResultObject("rejected", "Deployment rejected: policy violation - production deployments require approval", null, - null, null, null)); + null, null, null, null)); }); try (CopilotClient client = ctx.createClient()) { @@ -116,7 +116,7 @@ void testShouldHandleToolResultWithDeniedResultType() throws Exception { (invocation) -> { toolHandlerCalled[0] = true; return CompletableFuture.completedFuture(new ToolResultObject("denied", - "Access denied: insufficient permissions to read secrets", null, null, null, null)); + "Access denied: insufficient permissions to read secrets", null, null, null, null, null)); }); try (CopilotClient client = ctx.createClient()) { diff --git a/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java index 3e6291984f..56b8b281e6 100644 --- a/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java +++ b/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java @@ -32,7 +32,7 @@ public List definitions(ErgonomicTestTools instance, ObjectMappe Map args = invocation.getArguments(); String phase = (String) args.get("phase"); return CompletableFuture.completedFuture(instance.setCurrentPhase(phase)); - }, null, null, null), + }, null, null, null, null), new ToolDefinition( "search_items", "Search for items by keyword", Map .of("type", "object", "properties", @@ -44,11 +44,11 @@ public List definitions(ErgonomicTestTools instance, ObjectMappe Map args = invocation.getArguments(); String keyword = (String) args.get("keyword"); return CompletableFuture.completedFuture(instance.searchItems(keyword)); - }, null, null, null), + }, null, null, null, null), new ToolDefinition("get_status", "Returns the current status", Map.of("type", "object", "properties", Map.of(), "required", List.of()), invocation -> { return CompletableFuture.completedFuture(instance.getStatus()); - }, null, null, null), + }, null, null, null, null), new ToolDefinition("combine_values", "Combines two values into a single string", Map.of( "type", "object", "properties", Map .ofEntries( @@ -63,6 +63,6 @@ public List definitions(ErgonomicTestTools instance, ObjectMappe String value1 = (String) args.get("value1"); String value2 = (String) args.get("value2"); return CompletableFuture.completedFuture(instance.combineValues(value1, value2)); - }, null, null, null)); + }, null, null, null, null)); } } diff --git a/java/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/java/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java index 3fc22412e3..2b6b0164d5 100644 --- a/java/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java +++ b/java/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -321,11 +321,12 @@ void sessionModelGetCurrentParams_record() { @Test void sessionModelSwitchToParams_record() { - var params = new SessionModelSwitchToParams("sess-32", "claude-sonnet-4.5", "high", null, null, null); + var params = new SessionModelSwitchToParams("sess-32", "claude-sonnet-4.5", "high", null, null, null, null); assertEquals("sess-32", params.sessionId()); assertEquals("claude-sonnet-4.5", params.modelId()); assertEquals("high", params.reasoningEffort()); assertNull(params.reasoningSummary()); + assertNull(params.verbosity()); assertNull(params.modelCapabilities()); } @@ -803,7 +804,8 @@ 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 billing = new ModelBilling(1.0, null, null); + var promo = new ModelBillingPromo("summer-2026", 25.0, "2026-08-01T00:00:00Z", "Summer discount"); + var billing = new ModelBilling(1.0, null, null, promo); var modelItem = new Model("gpt-5", "GPT-5", capabilities, policy, billing, null, null, null, null); var result = new ModelsListResult(List.of(modelItem)); @@ -815,6 +817,10 @@ void modelsListResult_nested() { assertEquals(100000L, result.models().get(0).capabilities().limits().maxPromptTokens()); assertEquals(ModelPolicyState.ENABLED, result.models().get(0).policy().state()); assertEquals(Double.valueOf(1.0), result.models().get(0).billing().multiplier()); + assertEquals("summer-2026", result.models().get(0).billing().promo().id()); + 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()); } @Test @@ -836,7 +842,7 @@ 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, capabilities, null); + var params = new SessionModelSwitchToParams("sess-m", "gpt-5", null, null, null, capabilities, null); assertEquals("gpt-5", params.modelId()); assertNotNull(params.modelCapabilities()); diff --git a/java/src/test/java/com/github/copilot/rpc/ToolDefinitionFromObjectTest.java b/java/src/test/java/com/github/copilot/rpc/ToolDefinitionFromObjectTest.java index 37ea9f6a50..afa3d42511 100644 --- a/java/src/test/java/com/github/copilot/rpc/ToolDefinitionFromObjectTest.java +++ b/java/src/test/java/com/github/copilot/rpc/ToolDefinitionFromObjectTest.java @@ -92,6 +92,21 @@ void fromObject_handlerInvocation() throws Exception { assertEquals("Hello, Alice!", result); } + @Test + void fromObject_toolMetadata() { + var tools = ToolDefinition.fromObject(new SimpleTools()); + + var withMetadata = findTool(tools, "greet_user"); + assertNotNull(withMetadata); + assertNotNull(withMetadata.metadata()); + assertEquals(Map.of("github.com/copilot:safeForTelemetry", Map.of("name", true, "inputsNames", false)), + withMetadata.metadata()); + + var withoutMetadata = findTool(tools, "add_numbers"); + assertNotNull(withoutMetadata); + assertNull(withoutMetadata.metadata()); + } + // ── Test 2: Handler return type patterns ──────────────────────────────────── @Test diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/ArgCoercionTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/rpc/fixtures/ArgCoercionTools$$CopilotToolMeta.java index 882c0555f7..5cc5ee87a5 100644 --- a/java/src/test/java/com/github/copilot/rpc/fixtures/ArgCoercionTools$$CopilotToolMeta.java +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/ArgCoercionTools$$CopilotToolMeta.java @@ -45,6 +45,6 @@ public List definitions(ArgCoercionTools instance, ObjectMapper boolean flag = (Boolean) args.get("flag"); ArgCoercionTools.Color color = ArgCoercionTools.Color.valueOf((String) args.get("color")); return CompletableFuture.completedFuture(instance.mixedArgs(text, count, flag, color)); - }, null, null, null)); + }, null, null, null, null)); } } diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/DateTimeTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/rpc/fixtures/DateTimeTools$$CopilotToolMeta.java index 7001336504..0c2b1f07e7 100644 --- a/java/src/test/java/com/github/copilot/rpc/fixtures/DateTimeTools$$CopilotToolMeta.java +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/DateTimeTools$$CopilotToolMeta.java @@ -33,6 +33,6 @@ public List definitions(DateTimeTools instance, ObjectMapper map Map args = invocation.getArguments(); LocalDateTime when = mapper.convertValue(args.get("when"), LocalDateTime.class); return CompletableFuture.completedFuture(instance.scheduleEvent(when)); - }, null, null, null)); + }, null, null, null, null)); } } diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/DefaultValueTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/rpc/fixtures/DefaultValueTools$$CopilotToolMeta.java index ee5369b849..6cef2e03a0 100644 --- a/java/src/test/java/com/github/copilot/rpc/fixtures/DefaultValueTools$$CopilotToolMeta.java +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/DefaultValueTools$$CopilotToolMeta.java @@ -40,6 +40,6 @@ public List definitions(DefaultValueTools instance, ObjectMapper Object countRaw = args.containsKey("count") ? args.get("count") : 42; int count = ((Number) countRaw).intValue(); return CompletableFuture.completedFuture(instance.withDefault(label, count)); - }, null, null, null)); + }, null, null, null, null)); } } diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/InvocationAwareTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/rpc/fixtures/InvocationAwareTools$$CopilotToolMeta.java index cc7150e57c..e7c78608a0 100644 --- a/java/src/test/java/com/github/copilot/rpc/fixtures/InvocationAwareTools$$CopilotToolMeta.java +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/InvocationAwareTools$$CopilotToolMeta.java @@ -23,7 +23,7 @@ public List definitions(InvocationAwareTools instance, ObjectMap Map args = invocation.getArguments(); String phase = (String) args.get("phase"); return CompletableFuture.completedFuture(instance.reportProgress(phase, invocation)); - }, null, null, null), + }, null, null, null, null), new ToolDefinition("report_progress_async", "Reports progress asynchronously with invocation context", Map.of("type", "object", "properties", Map.ofEntries( @@ -33,7 +33,7 @@ public List definitions(InvocationAwareTools instance, ObjectMap Map args = invocation.getArguments(); String phase = (String) args.get("phase"); return instance.reportProgressAsync(phase, invocation).thenApply(r -> (Object) r); - }, null, null, null), + }, null, null, null, null), new ToolDefinition("report_progress_first", "Reports progress with invocation first", Map.of("type", "object", "properties", Map.ofEntries( @@ -43,11 +43,11 @@ public List definitions(InvocationAwareTools instance, ObjectMap Map args = invocation.getArguments(); String phase = (String) args.get("phase"); return CompletableFuture.completedFuture(instance.reportProgressFirst(invocation, phase)); - }, null, null, null), + }, null, null, null, null), new ToolDefinition("only_context", "Reports context with invocation only", Map.of("type", "object", "properties", Map.of(), "required", List.of()), invocation -> CompletableFuture.completedFuture(instance.onlyContext(invocation)), null, null, - null), + null, null), new ToolDefinition("report_progress_middle", "Reports progress with invocation in the middle", Map.of( "type", "object", "properties", Map.ofEntries(Map.entry("phase", Map.of("type", "string", "description", "Current phase")), @@ -58,7 +58,7 @@ public List definitions(InvocationAwareTools instance, ObjectMap int limit = ((Number) args.get("limit")).intValue(); return CompletableFuture .completedFuture(instance.reportProgressMiddle(phase, invocation, limit)); - }, null, null, null), + }, null, null, null, null), new ToolDefinition("report_progress_with_record", "Reports progress with record args and invocation", Map.of("type", "object", "properties", Map.ofEntries(Map.entry("query", Map.of("type", "string")), @@ -69,6 +69,6 @@ public List definitions(InvocationAwareTools instance, ObjectMap RecordInvocationArgs.class); return CompletableFuture .completedFuture(instance.reportProgressWithRecord(args, invocation)); - }, null, null, null)); + }, null, null, null, null)); } } diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/MultiReturnTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/rpc/fixtures/MultiReturnTools$$CopilotToolMeta.java index e1ac5c38dd..571db8e7cb 100644 --- a/java/src/test/java/com/github/copilot/rpc/fixtures/MultiReturnTools$$CopilotToolMeta.java +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/MultiReturnTools$$CopilotToolMeta.java @@ -16,14 +16,14 @@ public List definitions(MultiReturnTools instance, ObjectMapper return List.of(new ToolDefinition("string_method", "Returns a string", Map.of("type", "object", "properties", Map.of(), "required", List.of()), invocation -> { return CompletableFuture.completedFuture(instance.stringMethod()); - }, null, null, null), new ToolDefinition("void_method", "Void method", + }, null, null, null, null), new ToolDefinition("void_method", "Void method", Map.of("type", "object", "properties", Map.of(), "required", List.of()), invocation -> { instance.voidMethod(); return CompletableFuture.completedFuture("Success"); - }, null, null, null), + }, null, null, null, null), new ToolDefinition("async_method", "Async method", Map.of("type", "object", "properties", Map.of(), "required", List.of()), invocation -> { return instance.asyncMethod().thenApply(r -> (Object) r); - }, null, null, null)); + }, null, null, null, null)); } } diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/OptionalParamTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/rpc/fixtures/OptionalParamTools$$CopilotToolMeta.java index df6c39fd66..75fde6bb3c 100644 --- a/java/src/test/java/com/github/copilot/rpc/fixtures/OptionalParamTools$$CopilotToolMeta.java +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/OptionalParamTools$$CopilotToolMeta.java @@ -39,7 +39,7 @@ public List definitions(OptionalParamTools instance, ObjectMappe Object titleRaw = args.get("title"); Optional title = titleRaw != null ? Optional.of((String) titleRaw) : Optional.empty(); return CompletableFuture.completedFuture(instance.greetWithTitle(name, title)); - }, null, null, null), + }, null, null, null, null), new ToolDefinition("multiply", "Multiply with optional factor", Map.of("type", "object", "properties", Map.ofEntries( @@ -58,7 +58,7 @@ public List definitions(OptionalParamTools instance, ObjectMappe ? OptionalInt.of(((Number) factorRaw).intValue()) : OptionalInt.empty(); return CompletableFuture.completedFuture(instance.multiply(base, factor)); - }, null, null, null), + }, null, null, null, null), new ToolDefinition("scale", "Scale with optional ratio", Map.of("type", "object", "properties", Map.ofEntries( @@ -77,7 +77,7 @@ public List definitions(OptionalParamTools instance, ObjectMappe ? OptionalDouble.of(((Number) ratioRaw).doubleValue()) : OptionalDouble.empty(); return CompletableFuture.completedFuture(instance.scale(value, ratio)); - }, null, null, null), + }, null, null, null, null), new ToolDefinition("offset", "Offset with optional delta", Map.of("type", "object", "properties", Map.ofEntries( @@ -96,6 +96,6 @@ public List definitions(OptionalParamTools instance, ObjectMappe ? OptionalLong.of(((Number) deltaRaw).longValue()) : OptionalLong.empty(); return CompletableFuture.completedFuture(instance.offset(base, delta)); - }, null, null, null)); + }, null, null, null, null)); } } diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/OverrideTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/rpc/fixtures/OverrideTools$$CopilotToolMeta.java index 127dc922b4..2d37204f82 100644 --- a/java/src/test/java/com/github/copilot/rpc/fixtures/OverrideTools$$CopilotToolMeta.java +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/OverrideTools$$CopilotToolMeta.java @@ -34,6 +34,6 @@ public List definitions(OverrideTools instance, ObjectMapper map Map args = invocation.getArguments(); String pattern = (String) args.get("pattern"); return CompletableFuture.completedFuture(instance.customGrep(pattern)); - }, Boolean.TRUE, null, null)); + }, Boolean.TRUE, null, null, null)); } } diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/SimpleTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/rpc/fixtures/SimpleTools$$CopilotToolMeta.java index 0b52bd1ef2..ac38d0cce2 100644 --- a/java/src/test/java/com/github/copilot/rpc/fixtures/SimpleTools$$CopilotToolMeta.java +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/SimpleTools$$CopilotToolMeta.java @@ -30,7 +30,9 @@ public List definitions(SimpleTools instance, ObjectMapper mappe Map args = invocation.getArguments(); String name = (String) args.get("name"); return CompletableFuture.completedFuture(instance.greetUser(name)); - }, null, null, null), + }, null, null, null, + Map.of("github.com/copilot:safeForTelemetry", + Map.of("name", true, "inputsNames", false))), new ToolDefinition("add_numbers", "Adds two numbers together", Map.of("type", "object", "properties", Map.ofEntries( @@ -46,6 +48,6 @@ public List definitions(SimpleTools instance, ObjectMapper mappe int a = ((Number) args.get("a")).intValue(); int b = ((Number) args.get("b")).intValue(); return CompletableFuture.completedFuture(instance.addNumbers(a, b)); - }, null, null, null)); + }, null, null, null, null)); } } diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/SimpleTools.java b/java/src/test/java/com/github/copilot/rpc/fixtures/SimpleTools.java index 5bc3d841e5..814b3883c3 100644 --- a/java/src/test/java/com/github/copilot/rpc/fixtures/SimpleTools.java +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/SimpleTools.java @@ -12,7 +12,10 @@ */ public class SimpleTools { - @CopilotTool("Greets a user by name") + @CopilotTool(value = "Greets a user by name", metadata = { + @CopilotTool.MetadataEntry(key = "github.com/copilot:safeForTelemetry", value = @CopilotTool.MetadataValue(flags = { + @CopilotTool.MetadataFlag(name = "name", value = true), + @CopilotTool.MetadataFlag(name = "inputsNames", value = false)}))}) public String greetUser(@CopilotToolParam(value = "The user's name", required = true) String name) { return "Hello, " + name + "!"; } diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/StaticInvocationTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/rpc/fixtures/StaticInvocationTools$$CopilotToolMeta.java index c2fd7d7f6c..2535d671e8 100644 --- a/java/src/test/java/com/github/copilot/rpc/fixtures/StaticInvocationTools$$CopilotToolMeta.java +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/StaticInvocationTools$$CopilotToolMeta.java @@ -24,6 +24,6 @@ public List definitions(StaticInvocationTools instance, ObjectMa Map args = invocation.getArguments(); String phase = (String) args.get("phase"); return CompletableFuture.completedFuture(StaticInvocationTools.reportStatic(phase, invocation)); - }, null, null, null)); + }, null, null, null, null)); } } diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/StaticTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/rpc/fixtures/StaticTools$$CopilotToolMeta.java index 842547b68d..a0c6e66855 100644 --- a/java/src/test/java/com/github/copilot/rpc/fixtures/StaticTools$$CopilotToolMeta.java +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/StaticTools$$CopilotToolMeta.java @@ -32,6 +32,6 @@ public List definitions(StaticTools instance, ObjectMapper mappe // Mimics what the processor now generates for static methods: // QualifiedClassName.method(...) instead of instance.method(...) return CompletableFuture.completedFuture(StaticTools.greet(name)); - }, null, null, null)); + }, null, null, null, null)); } } diff --git a/java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.java b/java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.java index c2bda9f9ad..574d3acaa0 100644 --- a/java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.java +++ b/java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.java @@ -212,6 +212,88 @@ public String doSomething(@CopilotToolParam("Input") String input) { "Expected completedFuture wrapping for String return, got:\n" + generated); } + @Test + void generatesMetadata_withNestedFlags() { + String source = """ + package test; + import com.github.copilot.tool.CopilotTool; + import com.github.copilot.tool.CopilotToolParam; + public class MetaTools { + @CopilotTool(value = "Reports phase", metadata = { + @CopilotTool.MetadataEntry( + key = "github.com/copilot:safeForTelemetry", + value = @CopilotTool.MetadataValue(flags = { + @CopilotTool.MetadataFlag(name = "name", value = true), + @CopilotTool.MetadataFlag(name = "inputsNames", value = false) + })) + }) + public String reportPhase(@CopilotToolParam("Phase") String phase) { + return phase; + } + } + """; + + CompilationResult result = compileWithProcessor(List.of(inMemorySource("test.MetaTools", source))); + assertNoErrors(result); + String generated = result.getGeneratedSource("test.MetaTools$$CopilotToolMeta"); + assertTrue(generated.contains("Map.of(\"github.com/copilot:safeForTelemetry\""), + "Expected typed metadata map, got:\n" + generated); + assertTrue(generated.contains("Map.of(\"name\", true, \"inputsNames\", false)"), + "Expected nested flag map, got:\n" + generated); + } + + @Test + void generatesNullMetadata_whenAbsent() { + String source = """ + package test; + import com.github.copilot.tool.CopilotTool; + import com.github.copilot.tool.CopilotToolParam; + public class PlainTools { + @CopilotTool("Plain tool") + public String doSomething(@CopilotToolParam("Input") String input) { + return input; + } + } + """; + + CompilationResult result = compileWithProcessor(List.of(inMemorySource("test.PlainTools", source))); + assertNoErrors(result); + String generated = result.getGeneratedSource("test.PlainTools$$CopilotToolMeta"); + assertFalse(generated.contains("Map.of("), + "Expected no metadata map for a tool without metadata, got:\n" + generated); + assertTrue(generated.contains(" null\n )"), + "Expected metadata constructor argument to be null when metadata is absent, got:\n" + generated); + } + + @Test + void generatesMetadata_alongsideOtherFlags() { + String source = """ + package test; + import com.github.copilot.tool.CopilotTool; + import com.github.copilot.rpc.ToolDefer; + import com.github.copilot.tool.CopilotToolParam; + public class ComboTools { + @CopilotTool(value = "Combo", name = "combo", overridesBuiltInTool = true, + skipPermission = true, defer = ToolDefer.NEVER, + metadata = { + @CopilotTool.MetadataEntry(key = "k", + value = @CopilotTool.MetadataValue(bool = true)) + }) + public String doSomething(@CopilotToolParam("Input") String input) { + return input; + } + } + """; + + CompilationResult result = compileWithProcessor(List.of(inMemorySource("test.ComboTools", source))); + assertNoErrors(result); + String generated = result.getGeneratedSource("test.ComboTools$$CopilotToolMeta"); + assertTrue(generated.contains("Boolean.TRUE"), "Expected overrides/skip flags, got:\n" + generated); + assertTrue(generated.contains("ToolDefer.NEVER"), "Expected defer, got:\n" + generated); + assertTrue(generated.contains("Map.of(\"k\", true)"), + "Expected scalar bool metadata, got:\n" + generated); + } + @Test void generatesCorrectCode_forVoidReturnType() { String source = """ diff --git a/nodejs/README.md b/nodejs/README.md index e9d83c6df9..52b3d28053 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -84,9 +84,11 @@ new CopilotClient(options?: CopilotClientOptions) **Options:** - `connection?: RuntimeConnection` - How to connect to the Copilot runtime. Construct via the factory functions on `RuntimeConnection`: - - `RuntimeConnection.forStdio({ path?, args? })` (default) — spawn the runtime and communicate over its stdin/stdout. - - `RuntimeConnection.forTcp({ port?, connectionToken?, path?, args? })` — spawn the runtime as a TCP server. + - `RuntimeConnection.forStdio({ path?, args?, env? })` (default) — spawn the runtime and communicate over its stdin/stdout. + - `RuntimeConnection.forTcp({ port?, connectionToken?, path?, args?, env? })` — spawn the runtime as a TCP server. - `RuntimeConnection.forUri(url, { connectionToken? })` — connect to an already-running runtime (mutually exclusive with `gitHubToken`/`useLoggedInUser`). There is no top-level `cliUrl` shortcut; use this factory for URL-based connections. + - `RuntimeConnection.forInProcess()` — host the runtime in-process over its native C ABI (FFI). **Experimental.** Because the runtime shares this process, `env`, `telemetry`, and `workingDirectory` are rejected with this transport; set them on the host process instead. + - The child-process transports (`forStdio`/`forTcp`) also accept a per-connection `env`. Set it there or via the top-level `env` option — not both (setting both throws). - `mode?: "empty" | "copilot-cli"` - Defaulting strategy. Use `"empty"` for multi-user server mode; defaults to `"copilot-cli"`. - `workingDirectory?: string` - Working directory for the runtime process (default: current process cwd). - `baseDirectory?: string` - Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned runtime. When not set, the runtime defaults to `~/.copilot`. Ignored when connecting via `RuntimeConnection.forUri`. diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 60de5cf8aa..799053c0fa 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.69-2", + "@github/copilot": "^1.0.71", + "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, @@ -699,9 +700,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.69-2.tgz", - "integrity": "sha512-D/fvtb8RZVL0jbDjU5k7M2J08mr2eB0n7+NwUAJw/9+s1lmZBN6F3OqKh83Uo03d8oLW32ITJhqoxvs85pyiLw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.71.tgz", + "integrity": "sha512-F3axBi+sXSLYDJbxCBW36bM6MYKNC2rlyAf3Ivo/MjiHKKJW7j5AmaR1IRYS9Gt8r9mxOwWFM1cJFA+CuLaR8g==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -710,20 +711,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.69-2", - "@github/copilot-darwin-x64": "1.0.69-2", - "@github/copilot-linux-arm64": "1.0.69-2", - "@github/copilot-linux-x64": "1.0.69-2", - "@github/copilot-linuxmusl-arm64": "1.0.69-2", - "@github/copilot-linuxmusl-x64": "1.0.69-2", - "@github/copilot-win32-arm64": "1.0.69-2", - "@github/copilot-win32-x64": "1.0.69-2" + "@github/copilot-darwin-arm64": "1.0.71", + "@github/copilot-darwin-x64": "1.0.71", + "@github/copilot-linux-arm64": "1.0.71", + "@github/copilot-linux-x64": "1.0.71", + "@github/copilot-linuxmusl-arm64": "1.0.71", + "@github/copilot-linuxmusl-x64": "1.0.71", + "@github/copilot-win32-arm64": "1.0.71", + "@github/copilot-win32-x64": "1.0.71" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.69-2.tgz", - "integrity": "sha512-643mEEP/0FfZQvxNmg9UquVJv01gJ2QTdi2qabT/cPX2jLpgrPRjRvikX/XewAzGSUSzy4pyx5qyDYIr7TjUcw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.71.tgz", + "integrity": "sha512-mEWzyqbqRAWgyU7i2uuSRoVPx/TwaFQX0nZmw0bc30aJ0BnO7cy2kYQyCHw8ykmf/tfxT0xauZ6k0BOFmWizzQ==", "cpu": [ "arm64" ], @@ -737,9 +738,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.69-2.tgz", - "integrity": "sha512-Lx4+8lLJdI8bhA/pLWcRMuae3nxM7SivhWpS5lWsQyPrsdF5g1vYPtmo7ip3hc65jOxpzVWImc9FgQ8d5fKvOA==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.71.tgz", + "integrity": "sha512-Md9yEg406OBVBx3w4PeEj62TubulVLBcHleqmCoOoUmPgUxPZotUbrqz3rtbzADbXfrrD7JWvVsbd2UiNL194w==", "cpu": [ "x64" ], @@ -753,9 +754,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.69-2.tgz", - "integrity": "sha512-IheFTABphOz4QIqTfN0sLwF57yZtOm0IWJbgUtQe9WkiduD9L8Ii1EtJKbpVPygIgwX4LbcYTCyCMglZjLPliw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.71.tgz", + "integrity": "sha512-ykLJYOqBj3jRB5IJCDugLClAqbr7DmtTbUjlNY7+Jdq/n6i+d7xUQGclf1IWL5gnxbGQVAf+zkToD+sRM389Kg==", "cpu": [ "arm64" ], @@ -769,9 +770,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.69-2.tgz", - "integrity": "sha512-FHMSL5sUqH55U2kyaRaUGNGguyr5SI2ce5FriyPnVQWvYUSuRbdfEo9mC8jeONecD0sO0FQuWwbMk2/JkQOHTA==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.71.tgz", + "integrity": "sha512-pC0FNHG+BBwZd6yZlM85kkAGN+uJhM6o+THi76N2GnnSxmw7+remb1mvYxdgRVbdCm+LBUIbCKRWJLuMwrfb6A==", "cpu": [ "x64" ], @@ -785,9 +786,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.69-2.tgz", - "integrity": "sha512-c8yvsxEUNjwaQOU8zO0VFg7LWqTDDvx2SV3rzNz4FSCxTZf0SmcZw6TczsTauKO/0hAq5lfLH4d1pJkTLQ62Zw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.71.tgz", + "integrity": "sha512-hBmDljFTjacxqZTasCEy43H8EIzuXB/hHEBBCMFjhB9J00nIxsO6Dh0woTifKpx7knTYZdpTjjca3D0pAoZlUA==", "cpu": [ "arm64" ], @@ -801,9 +802,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.69-2.tgz", - "integrity": "sha512-9OQVqN3muGyBePmDY7jGsfhGfCAqvauSZqoWZfX+n28YtZrXTXR7IfGSuzBwZq/isOhopaIplmOc19ZYIMeCEw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.71.tgz", + "integrity": "sha512-CfTXU8pa5dxRz22xQzoi3TiG1PJo9+WR8PRDiPSdkIBSyPJ1NvX87DJmfXjTgeAfR+wkjt/p0keDCaBBVhNmUA==", "cpu": [ "x64" ], @@ -817,9 +818,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.69-2.tgz", - "integrity": "sha512-eEKJid8T+tI70dn+2UL4s2b2AQcoHaAmaJ6x/7qTksdVuaII27vuVO/yE4wpWkhYl+yvI3Ml/nkavgrms7Y/Pw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.71.tgz", + "integrity": "sha512-+HI1DokixXhHUahj06Fw67ZAigBuXKC58BFma4UJOGrQsDgwOSbqeTQHCw6vuymzjKlg3sactfsCUTaefkjscQ==", "cpu": [ "arm64" ], @@ -833,9 +834,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.69-2.tgz", - "integrity": "sha512-usFpfQudpOEft/OYQaJ+c0MJOgP9bXZ1vctx5mRgC6d8+0dIRbLCZ5rVuir1K7zyS246uYHZgO/t0sMQH7pOdQ==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.71.tgz", + "integrity": "sha512-02kXOBd9CwBbCaztuf71WYWn+uGapCuiaasomN4tcMH3HBVZ4gi3J0ZUoRcgcS80xh81uQyeBHbnUKzb/RE/9A==", "cpu": [ "x64" ], @@ -921,6 +922,230 @@ "dev": true, "license": "MIT" }, + "node_modules/@koromix/koffi-darwin-arm64": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koromix/koffi-darwin-arm64/-/koffi-darwin-arm64-3.1.0.tgz", + "integrity": "sha512-VEt5r3fXTfbejr83PnuOP0H7s9Zmazcs+lofu96DOcRkistlMsn59wYyWiKpyAjs9PCgm0Ykh62ChZ3CGMmIOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-darwin-x64": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koromix/koffi-darwin-x64/-/koffi-darwin-x64-3.1.0.tgz", + "integrity": "sha512-n/tVRB9xIzdXT5H3zZt8ueThgWTSDL+yU7PWnU8wbZPBSawP/otx3swQyd6nMOqj1bmHgSHopiKSBXRS9pllmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-freebsd-arm64": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koromix/koffi-freebsd-arm64/-/koffi-freebsd-arm64-3.1.0.tgz", + "integrity": "sha512-vazoPYIhOAlXZksVIqDRMIID4VeUZKx8F3dR90hOobT2ATyOkqNS5dv5UCV7Q7DSq22lQTrdbvENBAhROzCp0w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-freebsd-ia32": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koromix/koffi-freebsd-ia32/-/koffi-freebsd-ia32-3.1.0.tgz", + "integrity": "sha512-Vm7Uc97ru6RTSVmae2zCZZQeaizqVZ8WoU4+gG4H03Qe+WOj7kbKt/MxT7VBzdbPYIU5ZJeG/ZED1YlZyab6eQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-freebsd-x64": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koromix/koffi-freebsd-x64/-/koffi-freebsd-x64-3.1.0.tgz", + "integrity": "sha512-N+VuVWjoiYPy1Go5mRadZ3B6RM5Qz+eCLhj2LXrMlefbUJ+O4gg7teCUGvPGfBEHDgmSN4yYUrfQmdJC10vOYw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-linux-arm64": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koromix/koffi-linux-arm64/-/koffi-linux-arm64-3.1.0.tgz", + "integrity": "sha512-Wx5iOkeALe2ympLdiYwRpIg5qUkyQIv8N2foZ9rRker0uE7ZtXew2RRkbEgMir4b0yDYR1zyXd6B62GUzLtZ/g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-linux-ia32": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koromix/koffi-linux-ia32/-/koffi-linux-ia32-3.1.0.tgz", + "integrity": "sha512-1DjYm1QehXU0dgn0uE+FGYOb3Of7GiTMqLS+ZI2gbl1b+h76sz4LRBvDVrQyAmSMVVU8/7696S21YgE/iBhBVg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-linux-loong64": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koromix/koffi-linux-loong64/-/koffi-linux-loong64-3.1.0.tgz", + "integrity": "sha512-NOa0LdyltdESz3oeTqUH6MErHVoJOHoeXIsEp6xIMTUh4eKXEtlDQeoK6EYqo0DnBt83Xud95qLvi4Aw12pG4Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-linux-riscv64": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koromix/koffi-linux-riscv64/-/koffi-linux-riscv64-3.1.0.tgz", + "integrity": "sha512-Ye6kiXZCGxGtAIXSly6XuOP5tJZNYOZ2eVg33k1MilKrzimAy9Mpw4d6e9+Sfsc1jesgeNYs1sb5iaI8HS3ncA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-linux-x64": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koromix/koffi-linux-x64/-/koffi-linux-x64-3.1.0.tgz", + "integrity": "sha512-3yQTOkQrMna4VX+yeyfYImBjLlGrItMpsWyfaW1uSiz/A6GRydqdwYH7DWnp4Z+RSGYZpsewkf7byMc8pOOQKA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-openbsd-ia32": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koromix/koffi-openbsd-ia32/-/koffi-openbsd-ia32-3.1.0.tgz", + "integrity": "sha512-/cDoFHb9yx4+yoT3GUpnKnfi3W2drG+/Ewo0TTZaQHb4PsxnYYyT6V8+t4cL5XXbQcTTcOsZxpmBRrn0NBa3dA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-openbsd-x64": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koromix/koffi-openbsd-x64/-/koffi-openbsd-x64-3.1.0.tgz", + "integrity": "sha512-CoQdqgnKvWgTXXZlUst8cBRQEov7QsxlTN2WAsu9wez01Xe6gEcH/zYePANualzzCbnaELfe5P0rA80QkoDuPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-win32-ia32": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koromix/koffi-win32-ia32/-/koffi-win32-ia32-3.1.0.tgz", + "integrity": "sha512-WjrA+DEkpy0xEHu48+NSOboHhTnzkIfsFuq3d/WrSs+T9WflWRng3jC7mdJxmR4eHb6i6BqjW3k/U0mNUTjFPA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-win32-x64": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koromix/koffi-win32-x64/-/koffi-win32-x64-3.1.0.tgz", + "integrity": "sha512-tnK5+IkzQBauQAQSzuyjso8OOIQRlaTZS39xIWpfqVYDLVDIuLDQk/WwHcOrR5yxlDrZq9ygiebBTOfcJFia7w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -2592,6 +2817,32 @@ "json-buffer": "3.0.1" } }, + "node_modules/koffi": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-3.1.0.tgz", + "integrity": "sha512-0mCvdjTJBXioiaKNz0vajAEdWtfM5qyhVXSq+wQrrU3odzNvl/J7Cqna79QpNo9mfoKpQgGsyFFDRtDACCwGrQ==", + "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", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", diff --git a/nodejs/package.json b/nodejs/package.json index 23aface8b4..4f588640a8 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -56,7 +56,8 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.69-2", + "@github/copilot": "^1.0.71", + "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index 8165b5a424..89c74c1535 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,8 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.69-2", + "@github/copilot": "^1.0.71", + "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 9f430600ca..61f4a99416 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -40,6 +40,7 @@ import type { } from "./generated/rpc.js"; import { getSdkProtocolVersion } from "./sdkProtocolVersion.js"; import { CopilotSession } from "./session.js"; +import type { FfiRuntimeHost } from "./ffiRuntimeHost.js"; import { createSessionFsAdapter, type SessionFsProvider } from "./sessionFsProvider.js"; import { createCopilotRequestAdapter } from "./copilotRequestHandler.js"; import type { CopilotRequestHandler } from "./copilotRequestHandler.js"; @@ -58,6 +59,7 @@ import type { BearerTokenProvider, GetStatusResponse, InternalRuntimeConnection, + RuntimeConnection, LargeToolOutputConfig, MCPServerConfig, ModelInfo, @@ -473,6 +475,7 @@ class TeardownResilientStreamMessageWriter extends StreamMessageWriter { export class CopilotClient { private cliStartTimeout: ReturnType | null = null; private cliProcess: ChildProcess | null = null; + private ffiHost: FfiRuntimeHost | null = null; private connection: MessageConnection | null = null; private messageWriter: TeardownResilientStreamMessageWriter | null = null; private socket: Socket | null = null; @@ -563,6 +566,32 @@ export class CopilotClient { } } + /** + * Environment variable that overrides the transport when the caller does not set + * {@link CopilotClientOptions.connection}. Accepts `"inprocess"` or `"stdio"` + * (case-insensitive); unset preserves the default stdio transport. Any other value + * is an error. + */ + private static readonly DEFAULT_CONNECTION_ENV_VAR = "COPILOT_SDK_DEFAULT_CONNECTION"; + + /** + * Resolves the default {@link RuntimeConnection} for the no-connection case, + * honoring {@link CopilotClient.DEFAULT_CONNECTION_ENV_VAR}. + */ + private static resolveDefaultConnection(): RuntimeConnection { + const value = process.env[CopilotClient.DEFAULT_CONNECTION_ENV_VAR]; + if (!value || value.toLowerCase() === "stdio") { + return { kind: "stdio" }; + } + if (value.toLowerCase() === "inprocess") { + return { kind: "inprocess" }; + } + throw new Error( + `Invalid ${CopilotClient.DEFAULT_CONNECTION_ENV_VAR} value '${value}'. ` + + `Expected 'inprocess', 'stdio', or unset.` + ); + } + /** * Creates a new CopilotClient instance. * @@ -594,8 +623,10 @@ export class CopilotClient { // Resolve the connection mode. `_internalConnection` is set by // `joinSession()` to opt into the parent-process stdio path; consumers // should always go through the public `connection` field. - const conn: InternalRuntimeConnection = options._internalConnection ?? - options.connection ?? { kind: "stdio" }; + const conn: InternalRuntimeConnection = + options._internalConnection ?? + options.connection ?? + CopilotClient.resolveDefaultConnection(); if ( conn.kind === "uri" && @@ -605,6 +636,40 @@ export class CopilotClient { "gitHubToken and useLoggedInUser cannot be used with RuntimeConnection.forUri (external server manages its own auth)" ); } + if (conn.kind === "inprocess" && options.workingDirectory !== undefined) { + throw new Error( + "workingDirectory is not supported with RuntimeConnection.forInProcess(): the in-process " + + "transport hosts the runtime in this process, so honoring it would require mutating the " + + "shared process-global cwd. Change the host process's working directory before " + + "constructing the client instead." + ); + } + if (conn.kind === "inprocess" && options.env !== undefined) { + throw new Error( + "env is not supported with RuntimeConnection.forInProcess(): the in-process transport loads " + + "the native runtime into the shared host process, whose single environment block cannot " + + "carry per-client values. Set the variables on the host process environment instead." + ); + } + if (conn.kind === "inprocess" && options.telemetry !== undefined) { + throw new Error( + "telemetry is not supported with RuntimeConnection.forInProcess(): telemetry configuration " + + "is lowered to environment variables read by native runtime code running in the shared " + + "host process, so per-client telemetry cannot be honored in-process. Configure telemetry " + + "via the host process environment, or use a child-process transport." + ); + } + if ( + (conn.kind === "stdio" || conn.kind === "tcp") && + conn.env !== undefined && + options.env !== undefined + ) { + throw new Error( + "Set environment variables via either the client-level env option or the connection's env " + + "(RuntimeConnection.forStdio/forTcp), not both. Prefer the connection-level env for " + + "child-process transports." + ); + } if (conn.kind === "tcp" && conn.connectionToken !== undefined) { if (typeof conn.connectionToken !== "string" || conn.connectionToken.length === 0) { throw new Error("connectionToken must be a non-empty string"); @@ -642,7 +707,13 @@ export class CopilotClient { this.onGitHubTelemetry = options.onGitHubTelemetry; this.setupClientGlobalHandlers(); - const effectiveEnv = options.env ?? process.env; + // Connection-level env (child-process transports only) takes precedence + // over the client-level env, which falls back to the ambient process env. + // The constructor guard above rejects setting both, so at most one of the + // first two is defined. Mirrors .NET/Python precedence. + const connEnv: Record | undefined = + conn.kind === "stdio" || conn.kind === "tcp" ? conn.env : undefined; + const effectiveEnv = connEnv ?? options.env ?? process.env; this.resolvedEnv = effectiveEnv; this.resolvedCliPath = conn.kind === "stdio" || conn.kind === "tcp" @@ -759,6 +830,11 @@ export class CopilotClient { private setupClientGlobalHandlers(): void { const handlers: import("./generated/rpc.js").ClientGlobalApiHandlers = {}; + // `hooks.invoke` is a client-global RPC method whose payload carries a + // `sessionId`; route each invocation to the matching session's dispatcher. + handlers.hooks = { + invoke: async (params) => await this.handleHooksInvoke(params), + }; if (this.requestHandler) { handlers.llmInference = createCopilotRequestAdapter(this.requestHandler, () => { if (!this.connection) { @@ -810,7 +886,9 @@ export class CopilotClient { try { // Only start CLI server process if not connecting to external server - if (!this.isExternalServer) { + if (this.connectionConfig.kind === "inprocess") { + await this.startInProcessFfi(); + } else if (!this.isExternalServer) { await this.startCLIServer(); } @@ -873,6 +951,20 @@ export class CopilotClient { // Disconnect all active sessions with retry logic const activeSessions = [...this.sessions.values()]; + // TEMPORARY: over the in-process (FFI) transport the runtime shares this + // process, so a turn still running when the runtime disposes the session + // can leave that session's SQLite session.db handle open — it isn't + // reclaimed by terminating a child process, so the file stays locked + // (Windows) and the session-state directory can't be removed. Abort any + // in-flight turn first so it cancels and releases the handle. Best-effort + // and idempotent: a session with no active turn is a no-op. Scoped to + // in-process only: stdio/tcp runtimes run in a child process that we kill + // on shutdown (which frees the handle), and for external servers we don't + // own the runtime and aborting would cancel pending work other clients + // may still resume. Remove once the runtime cleans up fully on shutdown. + if (this.connectionConfig.kind === "inprocess") { + await Promise.allSettled(activeSessions.map((session) => session.abort())); + } for (const session of activeSessions) { const sessionId = session.sessionId; let lastError: Error | null = null; @@ -910,7 +1002,7 @@ 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.isExternalServer) { + if (this.connection && (this.cliProcess || this.ffiHost) && !this.isExternalServer) { const runtimeShutdownStart = Date.now(); const shutdownPromise = this.rpc.runtime.shutdown(); void shutdownPromise.catch(() => undefined); @@ -1011,6 +1103,21 @@ export class CopilotClient { ); } } + // Tear down the in-process FFI host (closes the native connection and + // shuts down the native runtime host) for SDK-owned in-process runtimes. + if (this.ffiHost) { + const host = this.ffiHost; + this.ffiHost = null; + try { + host.dispose(); + } catch (error) { + errors.push( + new Error( + `Failed to dispose in-process runtime host: ${error instanceof Error ? error.message : String(error)}` + ) + ); + } + } if (this.cliStartTimeout) { clearTimeout(this.cliStartTimeout); this.cliStartTimeout = null; @@ -1113,6 +1220,16 @@ export class CopilotClient { this.cliProcess = null; } + // Tear down the in-process FFI host (if any). + if (this.ffiHost) { + try { + this.ffiHost.dispose(); + } catch { + // Ignore errors during force stop + } + this.ffiHost = null; + } + if (this.cliStartTimeout) { clearTimeout(this.cliStartTimeout); this.cliStartTimeout = null; @@ -1409,12 +1526,15 @@ export class CopilotClient { overridesBuiltInTool: tool.overridesBuiltInTool, skipPermission: tool.skipPermission, defer: tool.defer, + metadata: tool.metadata, })), + toolSearch: config.toolSearch, canvases: config.canvases?.map((canvas) => canvas.declaration), requestCanvasRenderer: config.requestCanvasRenderer, requestExtensions: config.requestExtensions, extensionSdkPath: config.extensionSdkPath, extensionInfo: config.extensionInfo, + canvasProvider: config.canvasProvider, commands: config.commands?.map((cmd) => ({ name: cmd.name, description: cmd.description, @@ -1472,6 +1592,7 @@ export class CopilotClient { remoteSession: config.remoteSession, cloud: config.cloud, expAssignments: config.expAssignments, + enableManagedSettings: config.enableManagedSettings, }); const { @@ -1625,12 +1746,15 @@ export class CopilotClient { overridesBuiltInTool: tool.overridesBuiltInTool, skipPermission: tool.skipPermission, defer: tool.defer, + metadata: tool.metadata, })), + toolSearch: config.toolSearch, canvases: config.canvases?.map((canvas) => canvas.declaration), requestCanvasRenderer: config.requestCanvasRenderer, requestExtensions: config.requestExtensions, extensionSdkPath: config.extensionSdkPath, extensionInfo: config.extensionInfo, + canvasProvider: config.canvasProvider, commands: config.commands?.map((cmd) => ({ name: cmd.name, description: cmd.description, @@ -1683,6 +1807,7 @@ export class CopilotClient { remoteSession: config.remoteSession, openCanvases: config.openCanvases, expAssignments: config.expAssignments, + enableManagedSettings: config.enableManagedSettings, }); const { workspacePath, capabilities, openCanvases } = response as { @@ -2195,6 +2320,47 @@ export class CopilotClient { }; } + /** + * Builds the environment for the spawned runtime child process (stdio/TCP): applies + * the auth token, connection token, `COPILOT_HOME`, keychain setting, and telemetry + * variables on top of the effective env. Not used by the in-process (FFI) transport, + * whose worker inherits the host process's ambient environment + * (see {@link CopilotClient.startInProcessFfi}). + */ + private buildRuntimeEnv(): Record { + const env: Record = { ...this.resolvedEnv }; + delete env.NODE_DEBUG; + + if (this.options.gitHubToken) { + env.COPILOT_SDK_AUTH_TOKEN = this.options.gitHubToken; + } + if (this.effectiveConnectionToken) { + env.COPILOT_CONNECTION_TOKEN = this.effectiveConnectionToken; + } + if (this.options.baseDirectory) { + env.COPILOT_HOME = this.options.baseDirectory; + } + // In empty mode, disable the system keychain. Keytar reads from a + // process-wide store that's shared across sessions, which is unsafe + // for multi-tenant hosts. The runtime falls back to file-based + // credential storage scoped to COPILOT_HOME. + if (this.options.mode === "empty") { + env.COPILOT_DISABLE_KEYTAR = "1"; + } + if (this.options.telemetry) { + const t = this.options.telemetry; + env.COPILOT_OTEL_ENABLED = "true"; + if (t.otlpEndpoint !== undefined) env.OTEL_EXPORTER_OTLP_ENDPOINT = t.otlpEndpoint; + if (t.otlpProtocol !== undefined) env.OTEL_EXPORTER_OTLP_PROTOCOL = t.otlpProtocol; + if (t.filePath !== undefined) env.COPILOT_OTEL_FILE_EXPORTER_PATH = t.filePath; + if (t.exporterType !== undefined) env.COPILOT_OTEL_EXPORTER_TYPE = t.exporterType; + if (t.sourceName !== undefined) env.COPILOT_OTEL_SOURCE_NAME = t.sourceName; + if (t.captureContent !== undefined) + env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = String(t.captureContent); + } + return env; + } + /** * Start the CLI server process */ @@ -2241,30 +2407,9 @@ export class CopilotClient { args.push("--remote"); } - // Suppress debug/trace output that might pollute stdout - const envWithoutNodeDebug = { ...this.resolvedEnv }; - delete envWithoutNodeDebug.NODE_DEBUG; - - // Set auth token in environment if provided - if (this.options.gitHubToken) { - envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.gitHubToken; - } - - if (this.effectiveConnectionToken) { - envWithoutNodeDebug.COPILOT_CONNECTION_TOKEN = this.effectiveConnectionToken; - } - - if (this.options.baseDirectory) { - envWithoutNodeDebug.COPILOT_HOME = this.options.baseDirectory; - } - - // In empty mode, disable the system keychain. Keytar reads from a - // process-wide store that's shared across sessions, which is unsafe - // for multi-tenant hosts. The runtime falls back to file-based - // credential storage scoped to COPILOT_HOME. - if (this.options.mode === "empty") { - envWithoutNodeDebug.COPILOT_DISABLE_KEYTAR = "1"; - } + // Suppress debug/trace output that might pollute stdout, and apply the + // shared runtime env (auth token, connection token, COPILOT_HOME, telemetry). + const envWithoutNodeDebug = this.buildRuntimeEnv(); if (!this.resolvedCliPath) { throw new Error( @@ -2276,26 +2421,6 @@ export class CopilotClient { ); } - // Set OpenTelemetry environment variables if telemetry is configured - if (this.options.telemetry) { - const t = this.options.telemetry; - envWithoutNodeDebug.COPILOT_OTEL_ENABLED = "true"; - if (t.otlpEndpoint !== undefined) - envWithoutNodeDebug.OTEL_EXPORTER_OTLP_ENDPOINT = t.otlpEndpoint; - if (t.otlpProtocol !== undefined) - envWithoutNodeDebug.OTEL_EXPORTER_OTLP_PROTOCOL = t.otlpProtocol; - if (t.filePath !== undefined) - envWithoutNodeDebug.COPILOT_OTEL_FILE_EXPORTER_PATH = t.filePath; - if (t.exporterType !== undefined) - envWithoutNodeDebug.COPILOT_OTEL_EXPORTER_TYPE = t.exporterType; - if (t.sourceName !== undefined) - envWithoutNodeDebug.COPILOT_OTEL_SOURCE_NAME = t.sourceName; - if (t.captureContent !== undefined) - envWithoutNodeDebug.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = String( - t.captureContent - ); - } - // Verify CLI exists before attempting to spawn if (!existsSync(this.resolvedCliPath)) { throw new Error( @@ -2432,12 +2557,120 @@ export class CopilotClient { return this.connectToParentProcessViaStdio(); case "stdio": return this.connectToChildProcessViaStdio(); + case "inprocess": + return this.connectViaFfi(); case "tcp": case "uri": return this.connectViaTcp(); } } + /** Starts the in-process FFI runtime with SDK-managed typed options. */ + private async startInProcessFfi(): Promise { + const entrypoint = this.resolveCliPathForFfi(); + // 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. + // The transpiled output is per-file (not bundled), so this resolves the + // sibling module at runtime in both the ESM and CJS builds. + const { FfiRuntimeHost } = await import("./ffiRuntimeHost.js"); + const environment: Record = {}; + if (this.options.gitHubToken) { + environment.COPILOT_SDK_AUTH_TOKEN = this.options.gitHubToken; + } + if (this.options.baseDirectory) { + environment.COPILOT_HOME = this.options.baseDirectory; + } + if (this.options.mode === "empty") { + environment.COPILOT_DISABLE_KEYTAR = "1"; + } + + const args: string[] = []; + if (this.options.logLevel) { + args.push("--log-level", this.options.logLevel); + } + if (this.options.gitHubToken) { + args.push("--auth-token-env", "COPILOT_SDK_AUTH_TOKEN"); + } + if (!this.options.useLoggedInUser) { + args.push("--no-auto-login"); + } + if (this.options.sessionIdleTimeoutSeconds > 0) { + args.push("--session-idle-timeout", this.options.sessionIdleTimeoutSeconds.toString()); + } + if (this.options.enableRemoteSessions) { + args.push("--remote"); + } + + const host = FfiRuntimeHost.create( + entrypoint, + CopilotClient.getNapiPrebuildsFolder(entrypoint), + environment, + args + ); + this.ffiHost = host; + await host.start(); + } + + /** + * Connect to the in-process FFI runtime host over its receive/send streams, + * reusing the same `vscode-jsonrpc` framing as the stdio transport. + */ + private async connectViaFfi(): Promise { + if (!this.ffiHost) { + throw new Error("In-process FFI runtime host not started"); + } + this.messageWriter = new TeardownResilientStreamMessageWriter(this.ffiHost.sendStream); + this.connection = createMessageConnection( + new StreamMessageReader(this.ffiHost.receiveStream), + this.messageWriter + ); + + this.attachConnectionHandlers(); + this.connection.listen(); + } + + /** + * Resolves the CLI entrypoint used for in-process FFI hosting: `COPILOT_CLI_PATH` + * when set, otherwise the bundled platform-package entrypoint. + */ + private resolveCliPathForFfi(): string { + return this.resolvedEnv.COPILOT_CLI_PATH ?? getBundledCliPath(); + } + + /** + * Returns the napi prebuilds folder name for the current host — the + * `-` convention (e.g. `win32-x64`, `darwin-arm64`, + * `linux-x64`, `linuxmusl-x64`) under which the runtime ships + * `prebuilds//runtime.node`. + */ + private static getNapiPrebuildsFolder(entrypoint: string): string { + const arch = process.arch; + if (arch !== "x64" && arch !== "arm64") { + throw new Error(`Unsupported architecture '${arch}' for in-process FFI hosting.`); + } + let platform: string = process.platform; + if (platform === "linux" && CopilotClient.isMusl(entrypoint)) { + platform = "linuxmusl"; + } + return `${platform}-${arch}`; + } + + private static isMusl(entrypoint: string): boolean { + if (entrypoint.includes(`copilot-linuxmusl-${process.arch}`)) { + return true; + } + if (entrypoint.includes(`copilot-linux-${process.arch}`)) { + return false; + } + const report = process.report?.getReport(); + const header = + report && "header" in report + ? (report.header as { glibcVersionRuntime?: string }) + : undefined; + return header !== undefined && header.glibcVersionRuntime === undefined; + } + /** * Connect to child via stdio pipes */ @@ -2568,15 +2801,6 @@ export class CopilotClient { await this.handleAutoModeSwitchRequest(params) ); - this.connection.onRequest( - "hooks.invoke", - async (params: { - sessionId: string; - hookType: string; - input: unknown; - }): Promise<{ output?: unknown }> => await this.handleHooksInvoke(params) - ); - this.connection.onRequest( "systemMessage.transform", async (params: { @@ -2620,8 +2844,9 @@ export class CopilotClient { } const session = this.sessions.get((notification as { sessionId: string }).sessionId); + const event = (notification as { event: SessionEvent }).event; if (session) { - session._dispatchEvent((notification as { event: SessionEvent }).event); + session._dispatchEvent(event); } } diff --git a/nodejs/src/copilotRequestHandler.ts b/nodejs/src/copilotRequestHandler.ts index a949cecfd9..ccfe6591c6 100644 --- a/nodejs/src/copilotRequestHandler.ts +++ b/nodejs/src/copilotRequestHandler.ts @@ -33,6 +33,9 @@ type InternalContext = CopilotRequestContext & { [kBridge]: CopilotWebSocketResp export interface CopilotRequestContext { readonly requestId: string; readonly sessionId?: string; + readonly agentId?: string; + readonly parentAgentId?: string; + readonly interactionType?: string; readonly transport: "http" | "websocket"; url: string; headers: LlmInferenceHeaders; @@ -249,6 +252,9 @@ export class CopilotRequestHandler { const ctx: InternalContext = { requestId: exchange.requestId, sessionId: exchange.sessionId, + agentId: exchange.agentId, + parentAgentId: exchange.parentAgentId, + interactionType: exchange.interactionType, transport: exchange.transport, url: exchange.url, headers: exchange.headers, @@ -456,6 +462,9 @@ interface BodyQueueItem { class CopilotRequestExchange { readonly requestId: string; sessionId?: string; + agentId?: string; + parentAgentId?: string; + interactionType?: string; method = "GET"; url = ""; headers: LlmInferenceHeaders = {}; @@ -478,6 +487,9 @@ class CopilotRequestExchange { /** Fill in the request context once the matching start frame arrives. */ setContext(params: LlmInferenceHttpRequestStartRequest): void { this.sessionId = params.sessionId; + this.agentId = params.agentId; + this.parentAgentId = params.parentAgentId; + this.interactionType = params.interactionType; this.method = params.method; this.url = params.url; this.headers = params.headers; diff --git a/nodejs/src/ffiRuntimeHost.ts b/nodejs/src/ffiRuntimeHost.ts new file mode 100644 index 0000000000..a92aa1589a --- /dev/null +++ b/nodejs/src/ffiRuntimeHost.ts @@ -0,0 +1,341 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** + * Hosts the Copilot runtime in-process by loading the native `runtime.node` cdylib + * and speaking JSON-RPC over its C ABI (FFI) instead of spawning a CLI child process + * and communicating over stdio/TCP. + * + * The native `host_start` export spawns the CLI worker itself + * (`node --embedded-host` for a `.js` entrypoint, or ` + * --embedded-host` for a packaged binary), so the SDK never launches the worker + * directly. LSP `Content-Length:`-framed JSON-RPC bytes are pumped across the ABI: + * writes go to `connection_write`; inbound frames arrive on a native callback that + * feeds {@link FfiRuntimeHost.receiveStream}. The existing `vscode-jsonrpc` + * `StreamMessageReader`/`StreamMessageWriter` handle framing unchanged — this is a + * transport swap, not a new protocol. + */ + +import { existsSync } from "node:fs"; +import koffi from "koffi"; +import { dirname, join, resolve } from "node:path"; +import { PassThrough, Writable } from "node:stream"; + +const SYMBOL_PREFIX = "copilot_runtime_"; + +// A long, referenced no-op timer keeps the Node event loop alive while the in-process +// connection is open (see start()); the exact interval is irrelevant. +const KEEP_ALIVE_INTERVAL_MS = 1 << 30; + +type KoffiFunction = ReturnType["func"]>; +type KoffiType = ReturnType; +type KoffiRegisteredCallback = ReturnType; + +interface FfiLibrary { + hostStart: KoffiFunction; + hostShutdown: KoffiFunction; + connectionOpen: KoffiFunction; + connectionWrite: KoffiFunction; + connectionClose: KoffiFunction; + outboundCallbackType: KoffiType; +} + +let loadedLibraryPath: string | undefined; +let loadedLibrary: FfiLibrary | undefined; + +/** + * Loads the cdylib once per process and binds the C ABI exports. Loading a + * different library path in the same process is unsupported. + */ +function loadLibrary(libraryPath: string): FfiLibrary { + if (loadedLibrary) { + if (loadedLibraryPath !== libraryPath) { + throw new Error( + `An in-process FFI runtime library is already loaded from '${loadedLibraryPath}'; ` + + `loading a different library from '${libraryPath}' in the same process is not supported.` + ); + } + return loadedLibrary; + } + + const lib = koffi.load(libraryPath); + const outboundCallbackType = koffi.pointer( + koffi.proto( + `void ${SYMBOL_PREFIX}outbound(void *userData, uint8 *bytesPtr, size_t bytesLen)` + ) + ); + + loadedLibrary = { + hostStart: lib.func(`${SYMBOL_PREFIX}host_start`, "uint32", [ + "uint8*", + "size_t", + "uint8*", + "size_t", + ]), + hostShutdown: lib.func(`${SYMBOL_PREFIX}host_shutdown`, "bool", ["uint32"]), + connectionOpen: lib.func(`${SYMBOL_PREFIX}connection_open`, "uint32", [ + "uint32", + outboundCallbackType, + "void*", + "uint8*", + "size_t", + "uint8*", + "size_t", + "uint8*", + "size_t", + ]), + connectionWrite: lib.func(`${SYMBOL_PREFIX}connection_write`, "bool", [ + "uint32", + "uint8*", + "size_t", + ]), + connectionClose: lib.func(`${SYMBOL_PREFIX}connection_close`, "bool", ["uint32"]), + outboundCallbackType, + }; + loadedLibraryPath = libraryPath; + return loadedLibrary; +} + +function buildArgvJson(cliEntrypoint: string, args: readonly string[]): Buffer { + // A `.js` entrypoint is launched via node; the packaged single-file CLI binary + // embeds its own Node and is invoked directly. `--no-auto-update` pins the worker + // to the bundled pkg matching the loaded cdylib, instead of drifting to a newer + // version installed under the user's `~/.copilot/pkg` (which would cause ABI skew). + const argv = cliEntrypoint.toLowerCase().endsWith(".js") + ? ["node", cliEntrypoint, "--embedded-host", "--no-auto-update"] + : [cliEntrypoint, "--embedded-host", "--no-auto-update"]; + argv.push(...args); + return Buffer.from(JSON.stringify(argv), "utf8"); +} + +function buildEnvJson(environment?: Record): Buffer | null { + if (!environment) { + return null; + } + const obj: Record = {}; + for (const [key, value] of Object.entries(environment)) { + if (value !== undefined) { + obj[key] = value; + } + } + if (Object.keys(obj).length === 0) { + return null; + } + return Buffer.from(JSON.stringify(obj), "utf8"); +} + +export class FfiRuntimeHost { + private readonly lib: FfiLibrary; + private serverId = 0; + private connectionId = 0; + private disposed = false; + private outboundCallback: KoffiRegisteredCallback | undefined; + private keepAliveTimer: ReturnType | undefined; + + /** The stream JSON-RPC reads server→client frames from. */ + readonly receiveStream: PassThrough; + /** The stream JSON-RPC writes client→server frames to. */ + readonly sendStream: Writable; + + private constructor( + private readonly libraryPath: string, + private readonly cliEntrypoint: string, + private readonly environment: Record | undefined, + private readonly args: readonly string[] + ) { + this.lib = loadLibrary(libraryPath); + this.receiveStream = new PassThrough(); + this.sendStream = new Writable({ + // connection_write enqueues the frame into the runtime's inbound channel and + // returns immediately, so a synchronous FFI call is sufficient here. + write: (chunk: Buffer, _encoding, callback) => { + try { + this.writeFrame(chunk); + callback(); + } catch (error) { + callback(error as Error); + } + }, + }); + } + + /** + * Resolves the cdylib next to the given CLI entrypoint and prepares the FFI host. + * The cdylib is resolved as `prebuilds//runtime.node` relative to + * the entrypoint directory (the napi-rs `-` layout, e.g. + * `linux-x64`). Throws if it cannot be found. + */ + static create( + cliEntrypoint: string, + prebuildsFolder: string, + environment: Record | undefined, + args: readonly string[] + ): FfiRuntimeHost { + const fullEntrypoint = resolve(cliEntrypoint); + const distDir = dirname(fullEntrypoint); + const libraryPath = join(distDir, "prebuilds", prebuildsFolder, "runtime.node"); + if (!existsSync(libraryPath)) { + throw new Error(`FFI runtime library not found. Looked for '${libraryPath}'.`); + } + return new FfiRuntimeHost(libraryPath, fullEntrypoint, environment, args); + } + + /** + * Starts the in-process runtime: spawns the CLI worker via the native host, + * waits for readiness, and opens the FFI JSON-RPC connection. + */ + async start(): Promise { + const argvJson = buildArgvJson(this.cliEntrypoint, this.args); + const envJson = buildEnvJson(this.environment); + + // The native host spawns the CLI worker itself and has no cwd parameter, so the + // worker inherits this process's cwd. A custom working directory is intentionally + // unsupported for the in-process transport (rejected by the client constructor) + // rather than mutating the shared process-global cwd here. + + // host_start blocks until the worker connects back and signals readiness + // (up to ~30s); run it as an async FFI call so the Node event loop isn't blocked. + this.serverId = await new Promise((resolvePromise, rejectPromise) => { + this.lib.hostStart.async( + argvJson, + argvJson.length, + envJson, + envJson ? envJson.length : 0, + (error: Error | null, result: number) => { + if (error) { + rejectPromise(error); + } else { + resolvePromise(result); + } + } + ); + }); + if (!this.serverId) { + throw new Error( + `copilot_runtime_host_start failed (library '${this.libraryPath}', entrypoint '${this.cliEntrypoint}').` + ); + } + + this.outboundCallback = koffi.register( + (_userData: unknown, bytesPtr: unknown, bytesLen: number | bigint) => + this.feedInbound(bytesPtr, bytesLen), + this.lib.outboundCallbackType + ); + + this.connectionId = this.lib.connectionOpen( + this.serverId, + this.outboundCallback, + null, + null, + 0, + null, + 0, + null, + 0 + ); + if (!this.connectionId) { + this.unregisterCallback(); + this.lib.hostShutdown(this.serverId); + this.serverId = 0; + throw new Error("copilot_runtime_connection_open failed."); + } + + // The in-process transport has no socket/pipe handle to keep the Node event loop + // alive while the SDK is idle awaiting a server→client frame. koffi delivers the + // outbound callback on the loop but does not reference it, so hold one referenced + // timer for the lifetime of the connection. + this.keepAliveTimer = setInterval(() => {}, KEEP_ALIVE_INTERVAL_MS); + } + + private writeFrame(frame: Buffer): void { + if (this.disposed || !this.connectionId) { + throw new Error("The in-process runtime connection is closed."); + } + const ok = this.lib.connectionWrite(this.connectionId, frame, frame.length); + if (!ok) { + throw new Error("Failed to write a frame to the in-process runtime connection."); + } + } + + /** + * Native outbound (server→client) callback. koffi delivers it on the JS event loop + * via a threadsafe function, so the frame is decoded and written straight to + * {@link receiveStream}. The native pointer is only valid for this call, so the + * bytes are copied out before returning. + */ + private feedInbound(bytesPtr: unknown, bytesLen: number | bigint): void { + // An exception thrown across the native→JS (Node-API) boundary cannot propagate + // and would surface only as a DEP0168 "uncaught Node-API callback exception" + // warning, so catch and log it here instead of letting it escape. + try { + // A native outbound callback can still be delivered on the event loop after + // dispose() has ended receiveStream; writing then would throw + // ERR_STREAM_WRITE_AFTER_END. Drop late frames instead — the connection is + // gone and nothing is reading them. + if (this.disposed || this.receiveStream.writableEnded) { + return; + } + const length = Number(bytesLen); + if (!bytesPtr || length <= 0) { + return; + } + const bytes = koffi.decode( + bytesPtr, + koffi.array("uint8", length, "Typed") + ) as Uint8Array; + this.receiveStream.write(Buffer.from(bytes)); + } catch (error) { + console.error( + `In-process FFI inbound callback failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}` + ); + } + } + + private unregisterCallback(): void { + if (this.outboundCallback === undefined) { + return; + } + const callback = this.outboundCallback; + this.outboundCallback = undefined; + try { + koffi.unregister(callback); + } catch { + // Ignore teardown failures. + } + } + + /** Closes the FFI connection, shuts down the native host, and releases resources. */ + dispose(): void { + if (this.disposed) { + return; + } + this.disposed = true; + + if (this.keepAliveTimer !== undefined) { + clearInterval(this.keepAliveTimer); + this.keepAliveTimer = undefined; + } + + try { + if (this.connectionId) { + this.lib.connectionClose(this.connectionId); + this.connectionId = 0; + } + } catch { + // Ignore teardown failures. + } + + try { + if (this.serverId) { + this.lib.hostShutdown(this.serverId); + this.serverId = 0; + } + } catch { + // Ignore teardown failures. + } + + this.receiveStream.end(); + this.unregisterCallback(); + } +} diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 2a5c0e70f9..255a769d55 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, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpServerSource, McpServerStatus, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, UserToolSessionApproval } from "./session-events.js"; +import type { AbortReason, Attachment, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpServerSource, McpServerStatus, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, UserToolSessionApproval, Verbosity } from "./session-events.js"; /** * Initial authentication info for the session. @@ -521,6 +521,47 @@ export type FilterMapping = [k: string]: ContentFilterMode; } | ContentFilterMode; +/** + * 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 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) * @@ -817,6 +858,18 @@ export type McpHeadersHandlePendingHeadersRefreshRequest = | { kind: "none"; }; +/** + * Consumer allowed to call an MCP tool. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpToolUiVisibility". + */ +/** @experimental */ +export type McpToolUiVisibility = + /** The model may call the tool. */ + | "model" + /** An MCP App view may call the tool. */ + | "app"; /** * Host response to the pending OAuth request. * @@ -3370,6 +3423,10 @@ export interface DiscoveredCanvas { * Short, single-sentence description shown to the agent in canvas catalogs. */ description: string; + /** + * Host-local PNG path for the canvas icon, when supplied + */ + icon?: string; inputSchema?: CanvasJsonSchema; /** * Actions the agent or host may invoke on an open instance @@ -3425,6 +3482,10 @@ export interface OpenCanvasInstance { * Provider-local canvas identifier */ canvasId: string; + /** + * Host-local PNG path for the canvas icon, when supplied + */ + icon?: string; /** * Rendered title */ @@ -5122,6 +5183,30 @@ export interface HistoryTruncateResult { */ eventsRemoved: number; } +/** + * Runtime-owned wire payload for a server-to-client hook callback invocation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HookInvokeRequest". + */ +/** @experimental */ +/** @internal */ +export interface HookInvokeRequest { + sessionId: string; + hookType: HookType; + input: unknown; +} +/** + * Optional output returned by an SDK callback hook. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HookInvokeResponse". + */ +/** @experimental */ +/** @internal */ +export interface HookInvokeResponse { + output?: unknown; +} /** * Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. * @@ -6620,7 +6705,7 @@ export interface McpListToolsResult { tools: McpTools[]; } /** - * MCP tool metadata with tool name and optional description. + * MCP tool metadata with tool name, optional description, and normalized MCP Apps discovery metadata. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "McpTools". @@ -6635,6 +6720,24 @@ export interface McpTools { * Tool description, when provided. */ description?: string; + ui?: McpToolUi; +} +/** + * Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpToolUi". + */ +/** @experimental */ +export interface McpToolUi { + /** + * URI of the tool's MCP App resource, typically a `ui://` resource identifier. Use `session.mcp.resources.read` to fetch its HTML and resource metadata. + */ + resourceUri?: string; + /** + * Tool visibility advertised by the server. When absent, MCP Apps defaults apply. + */ + visibility?: McpToolUiVisibility[]; } /** * Pending MCP OAuth request ID and host-provided token or cancellation response. @@ -6784,27 +6887,302 @@ export interface McpRemoveGitHubResult { removed: boolean; } /** - * Server name and opaque configuration for an individual MCP server restart. + * An MCP resource descriptor (spec `Resource`): URI, name, and optional title, description, MIME type, size, icons, annotations, and metadata. Server-provided fields outside the standard descriptor shape are exposed under `additionalProperties`. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "McpRestartServerRequest". + * via the `definition` "McpResource". */ /** @experimental */ -/** @internal */ -export interface McpRestartServerRequest { +export interface McpResource { /** - * Name of the MCP server to restart + * The resource URI (e.g. ui://... or file:///...) + */ + uri: string; + /** + * The programmatic name of the resource + */ + name: string; + /** + * Optional human-readable display title + */ + title?: string; + /** + * Optional description of what this resource represents + */ + description?: string; + /** + * MIME type of the resource, if known + */ + mimeType?: string; + /** + * Resource size in bytes, when known + */ + size?: number; + /** + * Icons associated with this resource + */ + icons?: McpResourceIcon[]; + annotations?: McpResourceAnnotations; + /** + * Resource-level metadata + */ + _meta?: { + [k: string]: unknown | undefined; + }; + /** + * Server-provided non-standard descriptor fields preserved from the MCP response + */ + additionalProperties?: { + [k: string]: unknown | undefined; + }; +} +/** + * A resource icon descriptor plus preserved non-standard icon fields. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpResourceIcon". + */ +/** @experimental */ +export interface McpResourceIcon { + /** + * Icon URI + */ + src: string; + /** + * Icon MIME type, when known + */ + mimeType?: string; + /** + * Icon sizes hint + */ + sizes?: string; + /** + * Theme hint for this icon + */ + theme?: string; + /** + * Server-provided non-standard icon fields preserved from the MCP response + */ + additionalProperties?: { + [k: string]: unknown | undefined; + }; +} +/** + * Standard MCP resource annotations plus preserved non-standard annotation fields. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpResourceAnnotations". + */ +/** @experimental */ +export interface McpResourceAnnotations { + /** + * Intended audience roles for this resource + */ + audience?: string[]; + /** + * Priority hint for model/client use + */ + priority?: number; + /** + * Last-modified timestamp hint + */ + lastModified?: string; + /** + * Server-provided non-standard annotation fields preserved from the MCP response + */ + additionalProperties?: { + [k: string]: unknown | undefined; + }; +} +/** + * MCP resource content with URI, optional MIME type, text or base64 blob, and resource metadata. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpResourceContent". + */ +/** @experimental */ +export interface McpResourceContent { + /** + * The resource URI + */ + uri: string; + /** + * MIME type of the content + */ + mimeType?: string; + /** + * Text content (e.g. HTML) + */ + text?: string; + /** + * Base64-encoded binary content + */ + blob?: string; + /** + * Resource-level metadata (CSP, permissions, etc.) + */ + _meta?: { + [k: string]: unknown | undefined; + }; +} +/** + * MCP server whose resources to enumerate. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpResourcesListRequest". + */ +/** @experimental */ +export interface McpResourcesListRequest { + /** + * Name of the MCP server whose resources to enumerate */ serverName: string; /** - * Opaque server configuration (MCPServerConfig). Marked internal: an in-process runtime shape supplied only by in-process CLI callers. - * - * @internal + * Opaque MCP pagination cursor from a prior `nextCursor` value */ - config: { + cursor?: string; +} +/** + * One page of resources advertised by the named MCP server. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpResourcesListResult". + */ +/** @experimental */ +export interface McpResourcesListResult { + /** + * Resources advertised by the server (proxied MCP `resources/list`) + */ + resources: McpResource[]; + /** + * Opaque cursor for the next page, if the server has more resources + */ + nextCursor?: string; +} +/** + * MCP server whose resource templates to enumerate. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpResourcesListTemplatesRequest". + */ +/** @experimental */ +export interface McpResourcesListTemplatesRequest { + /** + * Name of the MCP server whose resource templates to enumerate + */ + serverName: string; + /** + * Opaque MCP pagination cursor from a prior `nextCursor` value + */ + cursor?: string; +} +/** + * One page of resource templates advertised by the named MCP server. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpResourcesListTemplatesResult". + */ +/** @experimental */ +export interface McpResourcesListTemplatesResult { + /** + * Resource templates advertised by the server (proxied MCP `resources/templates/list`) + */ + resourceTemplates: McpResourceTemplate[]; + /** + * Opaque cursor for the next page, if the server has more resource templates + */ + nextCursor?: string; +} +/** + * An MCP resource template descriptor (spec `ResourceTemplate`): an RFC 6570 URI template, name, and optional title, description, MIME type, icons, annotations, and metadata. Server-provided fields outside the standard descriptor shape are exposed under `additionalProperties`. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpResourceTemplate". + */ +/** @experimental */ +export interface McpResourceTemplate { + /** + * An RFC 6570 URI template for constructing resource URIs + */ + uriTemplate: string; + /** + * The programmatic name of the resource template + */ + name: string; + /** + * Optional human-readable display title + */ + title?: string; + /** + * Optional description of what this template is for + */ + description?: string; + /** + * MIME type for resources matching this template, if uniform + */ + mimeType?: string; + /** + * Icons associated with resources matching this template + */ + icons?: McpResourceIcon[]; + annotations?: McpResourceAnnotations; + /** + * Resource-template-level metadata + */ + _meta?: { + [k: string]: unknown | undefined; + }; + /** + * Server-provided non-standard descriptor fields preserved from the MCP response + */ + additionalProperties?: { [k: string]: unknown | undefined; }; } +/** + * MCP server and resource URI to fetch. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpResourcesReadRequest". + */ +/** @experimental */ +export interface McpResourcesReadRequest { + /** + * Name of the MCP server hosting the resource + */ + serverName: string; + /** + * Resource URI + */ + uri: string; +} +/** + * Resource contents returned by the MCP server. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpResourcesReadResult". + */ +/** @experimental */ +export interface McpResourcesReadResult { + /** + * Resource contents returned by the server + */ + contents: McpResourceContent[]; +} +/** + * Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpRestartServerRequest". + */ +/** @experimental */ +export interface McpRestartServerRequest { + /** + * Name of the MCP server to restart + */ + serverName: string; + config?: McpServerConfig; +} /** * Outcome of an MCP sampling execution: success result, failure error, or cancellation. * @@ -6882,26 +7260,18 @@ export interface McpSetEnvValueModeResult { mode: McpSetEnvValueModeDetails; } /** - * Server name and opaque configuration for an individual MCP server start. + * Server name and configuration for an individual MCP server start. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "McpStartServerRequest". */ /** @experimental */ -/** @internal */ export interface McpStartServerRequest { /** * Name of the MCP server to start */ serverName: string; - /** - * Opaque server configuration (MCPServerConfig). Marked internal: an in-process runtime shape supplied only by in-process CLI callers. - * - * @internal - */ - config: { - [k: string]: unknown | undefined; - }; + config: McpServerConfig; } /** * MCP server startup filtering result. @@ -7141,7 +7511,7 @@ export interface SessionWorkingDirectoryContext { /** @experimental */ export interface MetadataRecordContextChangeResult {} /** - * Absolute path to set as the session's new working directory. + * Absolute path to set as the session's new working directory. For local sessions the path must be absolute and exist on disk: it is validated before any session state changes, and a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote sessions record the path as-is. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "MetadataSetWorkingDirectoryRequest". @@ -7154,7 +7524,7 @@ export interface MetadataSetWorkingDirectoryRequest { workingDirectory: string; } /** - * Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for `process.chdir` and any related side-effects (file index, etc.); this method only updates the session's own recorded path. + * Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects (file index, etc.); it does NOT change the process working directory (a session's cwd is per-session, not process-global). For local sessions the runtime validates the target first (an absolute path that exists on disk) and re-bases the permission primary directory; a rejected validation fails the call before anything is mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the new directory (best-effort). Remote sessions only record the path. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "MetadataSetWorkingDirectoryResult". @@ -7339,6 +7709,7 @@ export interface ModelBilling { * Whole-number percentage discount (0-100) applied to usage billed through this model. Populated for the synthetic `auto` model, where requests routed by auto-mode are billed at a reduced rate; absent for concrete models. */ discountPercent?: number; + promo?: ModelBillingPromo; } /** * Token-level pricing information for this model @@ -7423,6 +7794,31 @@ export interface ModelBillingTokenPricesLongContext { */ maxPromptTokens?: number; } +/** + * Active server-driven promotion for a model, including its discount and expiry. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelBillingPromo". + */ +/** @experimental */ +export interface ModelBillingPromo { + /** + * Stable identifier for the promotion campaign. + */ + id?: string; + /** + * Percentage discount (0-100) applied while the promotion is active. May be fractional. + */ + discountPercent?: number; + /** + * UTC ISO 8601 timestamp marking when the promotion ends. Always present: the API only surfaces a promo whose expiry parses and is in the future. Consumers should treat a past value as expired. + */ + endsAt: string; + /** + * Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it. + */ + message?: string; +} /** * Optional capability overrides (vision, tool_calls, reasoning, etc.). * @@ -7572,6 +7968,7 @@ export interface ModelSwitchToRequest { */ reasoningEffort?: string; reasoningSummary?: ReasoningSummary; + verbosity?: Verbosity; modelCapabilities?: ModelCapabilitiesOverride; contextTier?: ContextTier; } @@ -9302,7 +9699,7 @@ export interface PluginsInstallRequest { workingDirectory?: string; } /** - * Marketplace source to register. + * Marketplace source and optional working directory for relative-path resolution. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "PluginsMarketplacesAddRequest". @@ -9313,6 +9710,10 @@ export interface PluginsMarketplacesAddRequest { * Marketplace source. Accepts the same forms as the CLI: "owner/repo" or "owner/repo#ref" (GitHub), an http/https/ssh URL (optionally with #ref), a git scp-style URL (user@host:path), or a local path. The marketplace's own name (from its manifest) is used as the registration key. */ source: string; + /** + * Working directory used to resolve relative local paths in `source`. Defaults to the server's current working directory. + */ + workingDirectory?: string; } /** * Name of the marketplace whose plugin catalog to fetch. @@ -9372,6 +9773,10 @@ export interface PluginsReloadRequest { * Re-load user, plugin, and (subject to `deferRepoHooks`) repo hooks. Defaults to true. Has no effect when the host has not registered a hook reloader (e.g. remote sessions). */ reloadHooks?: boolean; + /** + * Re-discover and relaunch subprocess extensions (including plugin-shipped extensions) after refreshing plugins. Defaults to true. Has no effect when the session has no active extension controller (e.g. extensions were not requested for the session). + */ + reloadExtensions?: boolean; /** * When true, skip repo-level hooks during the hook reload. Use before folder trust is confirmed; load them post-trust via `sessions.loadDeferredRepoHooks`. */ @@ -10212,7 +10617,7 @@ export interface QueueRemoveMostRecentResult { /** @experimental */ export interface RegisterEventInterestParams { /** - * The event type the consumer wants the runtime to treat as 'observed' for behavior-switching gating. Some runtime code paths inspect whether any consumer is interested in a specific event type and choose a different implementation accordingly (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates the full interactive OAuth flow to the consumer; when no interest is registered the runtime installs a browserless fallback that silently reuses cached tokens). SDK clients that long-poll events do NOT automatically appear as listeners to these gating checks — they must explicitly call `registerInterest` for each event type they want the runtime to count as having a consumer. Multiple registrations for the same event type from the same or different consumers are tracked independently and must each be released. See: `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, `session_limits_exhausted.requested`, `user_input.requested`, `elicitation.requested`, `command.queued`, `exit_plan_mode.requested`. + * The event type the consumer wants the runtime to treat as 'observed' for behavior-switching gating. Some runtime code paths inspect whether any consumer is interested in a specific event type and choose a different implementation accordingly (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates interactive OAuth token acquisition to the consumer via `mcp.oauth_required` events; when no interest is registered the runtime still attempts non-interactive reconnect from cached or refreshable tokens, and only marks the server `needs-auth` if usable credentials are unavailable — it does not open a browser or start interactive OAuth without a consumer). SDK clients that long-poll events do NOT automatically appear as listeners to these gating checks — they must explicitly call `registerInterest` for each event type they want the runtime to count as having a consumer. Multiple registrations for the same event type from the same or different consumers are tracked independently and must each be released. See: `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, `session_limits_exhausted.requested`, `user_input.requested`, `elicitation.requested`, `command.queued`, `exit_plan_mode.requested`. */ eventType: string; } @@ -10413,6 +10818,12 @@ export interface RemoteControlStatusActive { promptManager?: { [k: string]: unknown | undefined; }; + /** + * True while a read-only/session-sync export is deferred, awaiting the first `user.message` before its MC session exists. Marked internal: this field is excluded from the public SDK surface and is populated only on the CLI in-process path. + * + * @internal + */ + awaitingFirstMessage?: boolean; } /** * The last setup attempt failed. The singleton is otherwise off. @@ -10870,6 +11281,93 @@ export interface SendAttachmentsToMessageParams { */ attachments: PushAttachment[]; } +/** + * A single user message to append to the session as part of a `session.sendMessages` turn + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SendMessageItem". + */ +/** @experimental */ +export interface SendMessageItem { + /** + * The user message text + */ + prompt: string; + /** + * If provided, this is shown in the timeline instead of `prompt` + */ + displayPrompt?: string; + /** + * Optional attachments (files, directories, selections, blobs, GitHub references) to include with this message + */ + attachments?: Attachment[]; + /** + * If false, this message will not trigger a Premium Request Unit charge. User messages default to billable. + * + * @internal + */ + billable?: boolean; + /** + * If set, the request will fail if the named tool is not available when this message is among the user messages at the start of the current exchange + */ + requiredTool?: string; + /** + * Optional provenance tag copied to the resulting user.message event. Must match one of three forms: the literal `system`, `command-` for messages originating from a command (e.g. slash command, Mission Control command), or `schedule-` for messages originating from a scheduled job. + * + * @internal + */ + source?: string; +} +/** + * Parameters for sending zero or more user messages to the session in a single turn. Remote-backed (Mission Control) sessions do not support this method and will return an error. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SendMessagesRequest". + */ +/** @experimental */ +export interface SendMessagesRequest { + /** + * The user messages to append to the conversation, in order. May be empty, in which case a single turn runs over the existing history with no new user message. + */ + messages: SendMessageItem[]; + mode?: SendMode; + /** + * If true, adds the messages to the front of the queue instead of the end + */ + prepend?: boolean; + agentMode?: SendAgentMode; + /** + * Custom HTTP headers to include in outbound model requests for this turn. Merged with session-level provider headers; per-turn headers augment and overwrite session-level headers with the same key. + */ + requestHeaders?: { + [k: string]: string | undefined; + }; + /** + * W3C Trace Context traceparent header for distributed tracing of this agent turn + */ + traceparent?: string; + /** + * W3C Trace Context tracestate header for distributed tracing + */ + tracestate?: string; + /** + * If true, await completion of the agentic loop for this turn before returning. Defaults to false (fire-and-forget). When true, the result still contains the same `messageIds`; the caller can rely on the agent having processed the messages before the call resolves. + */ + wait?: boolean; +} +/** + * Result of sending zero or more user messages + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SendMessagesResult". + */ +/** @experimental */ +export interface SendMessagesResult { + /** + * Unique identifiers assigned to the messages, one per provided message in order. Empty when no messages were provided. + */ + messageIds: string[]; +} /** * Parameters for sending a user message to the session * @@ -11733,6 +12231,10 @@ export interface SessionModelList { * Available models, ordered with the most preferred default first. Includes both Copilot (CAPI) models and any registry BYOK models; a BYOK model appears under its provider-qualified selection id (`provider/id`). */ list: unknown[]; + /** + * Cost categories for the full CAPI catalog, including picker-disabled models that Auto may select. Metadata only; entries absent from `list` are not manually selectable. + */ + modelPriceCategories?: SessionModelPriceCategory[]; /** * Per-quota snapshots returned alongside the model list, keyed by quota type. */ @@ -11740,6 +12242,17 @@ export interface SessionModelList { [k: string]: unknown | undefined; }; } +/** + * Cost-category metadata for a CAPI model. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionModelPriceCategory". + */ +/** @experimental */ +export interface SessionModelPriceCategory { + id: string; + priceCategory: ModelPickerPriceCategory; +} /** * Session construction options. * @@ -11765,6 +12278,7 @@ export interface SessionOpenOptions { */ reasoningEffort?: string; reasoningSummary?: SessionOpenOptionsReasoningSummary; + verbosity?: Verbosity; /** * Identifier of the client driving the session. */ @@ -11790,9 +12304,9 @@ export interface SessionOpenOptions { [k: string]: unknown | undefined; }; /** - * Opt-in: self-fetch enterprise managed settings at session bootstrap. + * Opt-in: self-fetch and enforce enterprise managed settings at session bootstrap. */ - selfFetchManagedSettings?: boolean; + enableManagedSettings?: boolean; /** * Feature-flag values resolved by the host. */ @@ -11851,6 +12365,10 @@ export interface SessionOpenOptions { * Denylist of tool names. */ excludedTools?: string[]; + /** + * 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. + */ + includedBuiltinAgents?: string[]; /** * Built-in subagent names to exclude from this session. Excluded built-ins are hidden from agent discovery and cannot be dispatched unless a custom agent with the same name is available. */ @@ -12154,6 +12672,14 @@ export interface SessionsOpenHandoff { onProgress?: { [k: string]: unknown | undefined; }; + /** + * In-process confirmation callback `(request) => boolean | Promise` invoked when the handoff needs the caller to confirm a non-fatal blocker (e.g. a repository mismatch between the current working directory and the remote session). Returning `true` proceeds with the handoff; returning `false` (or omitting the callback) aborts it. Marked internal because a function reference cannot cross the JSON-RPC boundary, for the same reasons as `onProgress`. + * + * @internal + */ + onConfirm?: { + [k: string]: unknown | undefined; + }; } /** * Result of opening a session. @@ -12906,6 +13432,7 @@ export interface SessionUpdateOptionsParams { */ reasoningEffort?: string; reasoningSummary?: OptionsUpdateReasoningSummary; + verbosity?: Verbosity; /** * Identifier of the client driving the session. */ @@ -12942,6 +13469,10 @@ export interface SessionUpdateOptionsParams { * Denylist of tool names for this session. */ excludedTools?: string[]; + /** + * 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. Set to null to remove the allowlist restriction. + */ + includedBuiltinAgents?: string[] | null; /** * Built-in subagent names to exclude from this session. Excluded built-ins are hidden from agent discovery and cannot be dispatched unless a custom agent with the same name is available. */ @@ -13101,6 +13632,10 @@ export interface SessionUpdateOptionsResult { * Whether the operation succeeded */ success: boolean; + /** + * Number of hooks loaded from installed plugins, returned when installedPlugins is updated + */ + pluginHookCount?: number; } /** * User-requested shell execution cancellation handle. @@ -13430,7 +13965,7 @@ export interface SkillsLoadDiagnostics { errors: string[]; } /** - * Slash-command invocation result that submits an agent prompt, with display prompt, optional mode, and settings-change flag. + * Slash-command invocation result that submits an agent prompt, with display prompt, optional mode, optional user-facing notice, and settings-change flag. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "SlashCommandAgentPromptResult". @@ -13450,6 +13985,10 @@ export interface SlashCommandAgentPromptResult { */ displayPrompt: string; mode?: SessionMode; + /** + * Optional user-facing notice to show before the prompt is submitted + */ + notice?: string; /** * True when the invocation mutated user runtime settings; consumers caching settings should refresh */ @@ -15520,7 +16059,7 @@ export function createServerRpc(connection: MessageConnection) { /** * Registers a new marketplace from a source (owner/repo, URL, or local path). * - * @param params Marketplace source to register. + * @param params Marketplace source and optional working directory for relative-path resolution. * * @returns Result of registering a new marketplace. */ @@ -15629,6 +16168,16 @@ export function createServerRpc(connection: MessageConnection) { connection.sendRequest("instructions.getDiscoveryPaths", params), }, /** @experimental */ + commands: { + /** + * Lists the well-known built-in slash commands that work as the first message in a new session (e.g. /plan, /env), without requiring an active session. Commands that depend on session state, authentication, or a synced session are omitted. + * + * @returns Slash commands available in the session, after applying any include/exclude filters. + */ + list: async (): Promise => + connection.sendRequest("commands.list", {}), + }, + /** @experimental */ user: { /** @experimental */ settings: { @@ -16012,6 +16561,17 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin */ send: async (params: SendRequest): Promise => connection.sendRequest("session.send", { sessionId, ...params }), + /** + * Sends zero or more user messages to the session in a single turn and returns their message IDs. All provided messages are appended to the conversation in order, then exactly one agent turn runs over the resulting history. When the list is empty, one turn runs over the existing history with no new user message. Remote-backed (Mission Control) sessions do not support this method and will return an error. + * + * @param params Parameters for sending zero or more user messages to the session in a single turn. Remote-backed (Mission Control) sessions do not support this method and will return an error. + * + * @returns Result of sending zero or more user messages + * + * @experimental + */ + sendMessages: async (params: SendMessagesRequest): Promise => + connection.sendRequest("session.sendMessages", { sessionId, ...params }), /** * Aborts the current agent turn. * @@ -16515,7 +17075,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin list: async (): Promise => connection.sendRequest("session.mcp.list", { sessionId }), /** - * Lists the tools exposed by a connected MCP server on this session's host. + * Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session. * * @param params Server name whose tool list should be returned. * @@ -16576,6 +17136,20 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin */ removeGitHub: async (): Promise => connection.sendRequest("session.mcp.removeGitHub", { sessionId }), + /** + * Starts an individual MCP server on the live session from a caller-supplied config. Session-scoped and ephemeral: the server is added to this session's running set only and is reaped when the session ends. Does NOT modify persistent user configuration (`mcp.config.*`), so it does not affect future sessions. The server surfaces through `session.mcp.list` and the `session.mcp_servers_loaded` / `session.mcp_server_status_changed` events like any other server. + * + * @param params Server name and configuration for an individual MCP server start. + */ + startServer: async (params: McpStartServerRequest): Promise => + connection.sendRequest("session.mcp.startServer", { sessionId, ...params }), + /** + * Restarts an individual MCP server on the live session (stops then starts). Omit `config` for a config-free restart-by-name of an already-configured server; supply `config` to restart with a replacement configuration. Session-scoped and ephemeral: does NOT modify persistent user configuration (`mcp.config.*`). + * + * @param params Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. + */ + restartServer: async (params: McpRestartServerRequest): Promise => + connection.sendRequest("session.mcp.restartServer", { sessionId, ...params }), /** * Stops an individual MCP server on the session's host. * @@ -16678,6 +17252,36 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin diagnose: async (params: McpAppsDiagnoseRequest): Promise => connection.sendRequest("session.mcp.apps.diagnose", { sessionId, ...params }), }, + /** @experimental */ + resources: { + /** + * Fetch an MCP resource from a connected server by URI (proxies MCP `resources/read`). + * + * @param params MCP server and resource URI to fetch. + * + * @returns Resource contents returned by the MCP server. + */ + read: async (params: McpResourcesReadRequest): Promise => + connection.sendRequest("session.mcp.resources.read", { sessionId, ...params }), + /** + * Enumerate one page of resources a connected MCP server exposes (proxies MCP `resources/list`). Pass `cursor` to continue from a prior result's `nextCursor`. + * + * @param params MCP server whose resources to enumerate. + * + * @returns One page of resources advertised by the named MCP server. + */ + list: async (params: McpResourcesListRequest): Promise => + connection.sendRequest("session.mcp.resources.list", { sessionId, ...params }), + /** + * Enumerate one page of resource templates a connected MCP server exposes (proxies MCP `resources/templates/list`). Pass `cursor` to continue from a prior result's `nextCursor`. + * + * @param params MCP server whose resource templates to enumerate. + * + * @returns One page of resource templates advertised by the named MCP server. + */ + listTemplates: async (params: McpResourcesListTemplatesRequest): Promise => + connection.sendRequest("session.mcp.resources.listTemplates", { sessionId, ...params }), + }, }, /** @experimental */ plugins: { @@ -17240,11 +17844,11 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin recordContextChange: async (params: MetadataRecordContextChangeRequest): Promise => connection.sendRequest("session.metadata.recordContextChange", { sessionId, ...params }), /** - * Updates the session's recorded working directory. + * Updates the session's working directory. For local sessions the target is validated first (an absolute path that exists on disk) and the permission primary directory is re-based; a rejected validation fails the call before any session state changes. * - * @param params Absolute path to set as the session's new working directory. + * @param params Absolute path to set as the session's new working directory. For local sessions the path must be absolute and exist on disk: it is validated before any session state changes, and a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote sessions record the path as-is. * - * @returns Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for `process.chdir` and any related side-effects (file index, etc.); this method only updates the session's own recorded path. + * @returns Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects (file index, etc.); it does NOT change the process working directory (a session's cwd is per-session, not process-global). For local sessions the runtime validates the target first (an absolute path that exists on disk) and re-bases the permission primary directory; a rejected validation fails the call before anything is mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the new directory (best-effort). Remote sessions only record the path. */ setWorkingDirectory: async (params: MetadataSetWorkingDirectoryRequest): Promise => connection.sendRequest("session.metadata.setWorkingDirectory", { sessionId, ...params }), @@ -17502,20 +18106,6 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI */ configureGitHub: async (params: McpConfigureGitHubRequest): Promise => connection.sendRequest("session.mcp.configureGitHub", { sessionId, ...params }), - /** - * Starts an individual MCP server on the session's host. - * - * @param params Server name and opaque configuration for an individual MCP server start. - */ - startServer: async (params: McpStartServerRequest): Promise => - connection.sendRequest("session.mcp.startServer", { sessionId, ...params }), - /** - * Restarts an individual MCP server on the session's host (stops then starts). - * - * @param params Server name and opaque configuration for an individual MCP server restart. - */ - restartServer: async (params: McpRestartServerRequest): Promise => - connection.sendRequest("session.mcp.restartServer", { sessionId, ...params }), /** * Registers a pre-connected external MCP client (e.g. IDE) on the session's host. The caller retains lifecycle ownership of the client and transport. Marked internal because the `client` and `transport` arguments are in-process MCP SDK instances that cannot be serialized across the JSON-RPC boundary; once the CLI moves on top of the SDK, external clients will be expressed as transport configs the runtime can construct itself. * @@ -17793,6 +18383,19 @@ export function registerClientSessionApiHandlers( }); } +/** Handler for `hooks` client global API methods. */ +/** @experimental */ +export interface HooksHandler { + /** + * Dispatches one SDK callback hook from the runtime to the connection that registered it. Internal transport plumbing: clients opt in through session initialization and the Rust hook processor owns ordering, policy, timeout, and callback routing. + * + * @param params Runtime-owned wire payload for a server-to-client hook callback invocation. + * + * @returns Optional output returned by an SDK callback hook. + */ + invoke(params: HookInvokeRequest): Promise; +} + /** Handler for `llmInference` client global API methods. */ /** @experimental */ export interface LlmInferenceHandler { @@ -17827,6 +18430,7 @@ export interface GitHubTelemetryHandler { /** All client global API handler groups. */ export interface ClientGlobalApiHandlers { + hooks?: HooksHandler; llmInference?: LlmInferenceHandler; gitHubTelemetry?: GitHubTelemetryHandler; } @@ -17842,6 +18446,11 @@ export function registerClientGlobalApiHandlers( connection: MessageConnection, handlers: ClientGlobalApiHandlers, ): void { + connection.onRequest("hooks.invoke", async (params: HookInvokeRequest) => { + const handler = handlers.hooks; + if (!handler) throw new Error("No hooks client-global handler registered"); + return handler.invoke(params); + }); connection.onRequest("llmInference.httpRequestStart", async (params: LlmInferenceHttpRequestStartRequest) => { const handler = handlers.llmInference; if (!handler) throw new Error("No llmInference client-global handler registered"); diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 2f846c7213..7581545a8e 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -40,8 +40,10 @@ export type SessionEvent = | PendingMessagesModifiedEvent | AssistantTurnStartEvent | AssistantIntentEvent + | AssistantServerToolProgressEvent | AssistantReasoningEvent | AssistantReasoningDeltaEvent + | AssistantToolCallDeltaEvent | AssistantStreamingDeltaEvent | AssistantMessageEvent | AssistantMessageStartEvent @@ -90,6 +92,8 @@ export type SessionEvent = | AutoModeSwitchCompletedEvent | SessionLimitsExhaustedRequestedEvent | SessionLimitsExhaustedCompletedEvent + | AutoModeResolvedEvent + | ManagedSettingsResolvedEvent | CommandsChangedEvent | CapabilitiesChangedEvent | ExitPlanModeRequestedEvent @@ -100,6 +104,9 @@ export type SessionEvent = | CustomAgentsUpdatedEvent | McpServersLoadedEvent | McpServerStatusChangedEvent + | McpToolsListChangedEvent + | McpResourcesListChangedEvent + | McpPromptsListChangedEvent | ExtensionsLoadedEvent | CanvasOpenedEvent | CanvasRegistryChangedEvent @@ -135,6 +142,16 @@ export type ReasoningSummary = | "concise" /** Request a detailed summary of the model's reasoning. */ | "detailed"; +/** + * Output verbosity level used for supported model calls (e.g. "low", "medium", "high") + */ +export type Verbosity = + /** A terse response was requested. */ + | "low" + /** A medium amount of response detail was requested. */ + | "medium" + /** A more detailed response was requested. */ + | "high"; /** * The type of operation performed on the autopilot objective state file */ @@ -271,6 +288,14 @@ export type UserMessageDelivery = | "steering" /** Enqueued while the agent was busy; processed as its own run afterward. */ | "queued"; +/** + * Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. + */ +export type AssistantMessageToolRequestType = + /** Standard function-style tool call. */ + | "function" + /** Custom grammar-based tool call. */ + | "custom"; /** * The system that produced a citation. */ @@ -287,14 +312,6 @@ export type CitationProvider = */ /** @experimental */ export type CitationLocation = CitationLocationChar | CitationLocationPage | CitationLocationBlock; -/** - * Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. - */ -export type AssistantMessageToolRequestType = - /** Standard function-style tool call. */ - | "function" - /** Custom grammar-based tool call. */ - | "custom"; /** * API endpoint used for this model call, matching CAPI supported_endpoints vocabulary */ @@ -620,6 +637,26 @@ export type SessionLimitsExhaustedResponseAction = | "unset" /** Leave the limit unchanged and cancel the blocked model request. */ | "cancel"; +/** + * Coarse request-difficulty bucket for UX explainability + */ +export type AutoModeResolvedReasoningBucket = + /** The request looks low-reasoning; a lighter model is appropriate. */ + | "low" + /** The request needs a moderate amount of reasoning. */ + | "medium" + /** The request looks high-reasoning; a stronger model is appropriate. */ + | "high"; +/** + * Which channel supplied the effective enterprise managed settings (highest-authority present layer wins wholesale) + */ +export type ManagedSettingsResolvedSource = + /** Account/org policy self-fetched from the GitHub managed-settings endpoint (higher authority). */ + | "server" + /** Device-level MDM policy discovered from plist/registry/file (lower authority). */ + | "device" + /** No managed policy is in force (no layer contributed). */ + | "none"; /** * Exit plan mode action */ @@ -792,6 +829,7 @@ export interface StartData { * ISO 8601 timestamp when the session was created */ startTime: string; + verbosity?: Verbosity; /** * Schema version number for the session event format */ @@ -920,6 +958,7 @@ export interface ResumeData { * True when this resume attached to a session that the runtime already had running in-memory (for example, an extension joining a session another client was actively driving). False (or omitted) for cold resumes — the runtime had to reconstitute the session from its persisted event log. */ sessionWasActive?: boolean; + verbosity?: Verbosity; } /** * Session event "session.remote_steerable_changed". Notifies that the session's remote steering capability has changed @@ -1456,11 +1495,13 @@ export interface ModelChangeData { */ previousReasoningEffort?: string; previousReasoningSummary?: ReasoningSummary; + previousVerbosity?: Verbosity; /** * Reasoning effort level after the model change, if applicable */ reasoningEffort?: string | null; reasoningSummary?: ReasoningSummary; + verbosity?: Verbosity; } /** * Session event "session.mode_changed". Agent mode change details including previous and new modes @@ -3121,6 +3162,53 @@ export interface AssistantIntentData { */ intent: string; } +/** + * Session event "assistant.server_tool_progress". Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message + */ +export interface AssistantServerToolProgressEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: AssistantServerToolProgressData; + /** + * 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.server_tool_progress". + */ + type: "assistant.server_tool_progress"; +} +/** + * Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message + */ +export interface AssistantServerToolProgressData { + /** + * Kind of hosted server tool that is running. Only `web_search` is emitted today. + */ + kind: string; + /** + * Position of the hosted tool call in the response output. Stable across the call's lifecycle events (unlike the provider's per-event item id, which CAPI rotates), so the host keys the live in-progress row on it. + */ + outputIndex: number; + /** + * Lifecycle status of the hosted call: `in_progress`, `searching`, or `completed`. + */ + status: string; +} /** * Session event "assistant.reasoning". Assistant reasoning content for timeline display with complete thinking text */ @@ -3207,6 +3295,54 @@ export interface AssistantReasoningDeltaData { */ reasoningId: string; } +/** + * Session event "assistant.tool_call_delta". Streaming tool-call input delta for incremental tool-call updates + */ +export interface AssistantToolCallDeltaEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: AssistantToolCallDeltaData; + /** + * 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.tool_call_delta". + */ + type: "assistant.tool_call_delta"; +} +/** + * Streaming tool-call input delta for incremental tool-call updates + */ +export interface AssistantToolCallDeltaData { + /** + * Raw provider tool input fragment to append for this tool call. Function/tool-use providers stream serialized JSON argument text (so newlines inside JSON string values may appear as escaped `\n` until the accumulated JSON is parsed); custom tool calls stream raw custom input. + */ + inputDelta: string; + /** + * Tool call ID this delta belongs to, matching the corresponding assistant.message tool request + */ + toolCallId: string; + /** + * Name of the tool being invoked, when known from the stream + */ + toolName?: string; + toolType?: AssistantMessageToolRequestType; +} /** * Session event "assistant.streaming_delta". Streaming response progress with cumulative byte count */ @@ -3290,6 +3426,10 @@ export interface AssistantMessageData { * @experimental */ citations?: Citations; + /** + * Client-minted request id (x-request-id header) echoed by the server. Distinct from requestId (x-github-request-id) and serviceRequestId (x-copilot-service-request-id). + */ + clientRequestId?: string; /** * The assistant's text response content */ @@ -4388,6 +4528,14 @@ export interface ToolExecutionCompleteData { * Whether this tool call was explicitly requested by the user rather than the assistant */ isUserRequested?: boolean; + /** + * FIDES IFC label projected from tool ingress metadata (MCP `CallToolResult._meta` or synthesized built-in ingress labels). Persisted as `{ ifc: ... }` so the label survives session resume, including model-visible failure results. Experimental. + * + * @experimental + */ + mcpMeta?: { + [k: string]: unknown | undefined; + }; /** * Model identifier that generated this tool call */ @@ -4463,6 +4611,14 @@ export interface ToolExecutionCompleteResult { * Full detailed tool result for UI/timeline display, preserving complete content such as diffs. Falls back to content when absent. */ detailedContent?: string; + /** + * FIDES IFC label projected from tool ingress metadata (MCP `CallToolResult._meta` or synthesized built-in ingress labels) — persisted as `{ ifc: ... }` (only the `ifc` key, not the whole `_meta`). Persisted so the FIDES IFC label survives session resume: the engine rehydrates accumulated taint by replaying these on load. Populated for ingress sources when FIDES IFC is on. Experimental. + * + * @experimental + */ + mcpMeta?: { + [k: string]: unknown | undefined; + }; /** * Structured content (arbitrary JSON) returned verbatim by the MCP tool */ @@ -5824,6 +5980,14 @@ export interface PermissionRequestWrite { * Complete new file contents for newly created files */ newFileContents?: string; + /** + * True when a built-in file tool (apply_patch / str_replace_editor) asked to write a path the sandbox filesystem policy would block, and the host opted in via sandbox.allowBypass. This is a request, not a grant: the write happens unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + */ + requestSandboxBypass?: boolean; + /** + * Justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + */ + requestSandboxBypassReason?: string; /** * Tool call ID that triggered this permission request */ @@ -5905,6 +6069,14 @@ export interface PermissionRequestUrl { * Permission kind discriminator */ kind: "url"; + /** + * 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. + */ + requestSandboxBypass?: boolean; + /** + * Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + */ + requestSandboxBypassReason?: string; /** * Tool call ID that triggered this permission request */ @@ -6212,6 +6384,14 @@ export interface PermissionPromptRequestUrl { * Prompt kind discriminator */ kind: "url"; + /** + * 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. + */ + requestSandboxBypass?: boolean; + /** + * Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + */ + requestSandboxBypassReason?: string; /** * Tool call ID that triggered this permission request */ @@ -7026,6 +7206,7 @@ export interface McpOauthRequiredEvent { * OAuth authentication request for an MCP server */ export interface McpOauthRequiredData { + httpResponse?: McpOauthHttpResponse; reason: McpOauthRequestReason; /** * Unique identifier for this OAuth request; used to respond via session.mcp.oauth.handlePendingRequest @@ -7046,6 +7227,36 @@ export interface McpOauthRequiredData { staticClientConfig?: McpOauthRequiredStaticClientConfig; wwwAuthenticateParams?: McpOauthWWWAuthenticateParams; } +/** + * Raw HTTP response details from the OAuth auth challenge, as observed by the runtime. + */ +export interface McpOauthHttpResponse { + /** + * Complete UTF-8 response body for host-specific challenge handling, including an empty string for an empty body. Omitted when the complete body is not valid UTF-8; body read failures fail the HTTP operation rather than exposing a partial response. + */ + body?: string; + /** + * HTTP response headers as observed by the runtime. Order and casing are transport-dependent, and duplicate header names may appear multiple times. + */ + headers: HeaderEntry[]; + /** + * HTTP status code returned with the auth challenge. + */ + statusCode: number; +} +/** + * Single HTTP header entry as a name/value pair. + */ +export interface HeaderEntry { + /** + * HTTP response header name as observed by the runtime. + */ + name: string; + /** + * HTTP response header value as observed by the runtime. + */ + value: string; +} /** * Static OAuth client configuration, if the server specifies one */ @@ -7702,6 +7913,130 @@ export interface SessionLimitsExhaustedResponse { */ maxAiCredits?: number; } +/** + * Session event "session.auto_mode_resolved". Auto Intent resolution: the concrete model the session settled on for the first prompt of an auto-mode session, and why. Lets SDK clients render the chosen model and the full reason it was picked. The core selection fields (chosenModel/reasoningBucket/categoryScores) are stable; the routing-analytics fields (predictedLabel/confidence/candidateModels) mirror the upstream intent service and may evolve, hence the event's experimental stability. + */ +/** @experimental */ +export interface AutoModeResolvedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: AutoModeResolvedData; + /** + * 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.auto_mode_resolved". + */ + type: "session.auto_mode_resolved"; +} +/** + * Auto Intent resolution: the concrete model the session settled on for the first prompt of an auto-mode session, and why. Lets SDK clients render the chosen model and the full reason it was picked. The core selection fields (chosenModel/reasoningBucket/categoryScores) are stable; the routing-analytics fields (predictedLabel/confidence/candidateModels) mirror the upstream intent service and may evolve, hence the event's experimental stability. + */ +/** @experimental */ +export interface AutoModeResolvedData { + /** + * Ordered candidate model list the router returned, when not a fallback + */ + candidateModels?: string[]; + /** + * Per-category classifier scores (0-1) behind the bucket: the granular HYDRA capability scores (reasoning, code_gen, debugging, tool_use), or the binary needs_reasoning/no_reasoning scores when HYDRA didn't run. Lets clients show a breakdown rather than just the bucket. + */ + categoryScores?: { + [k: string]: number | undefined; + }; + /** + * The concrete model the session will use after any intent refinement + */ + chosenModel: string; + /** + * Classifier confidence for the predicted label, when available + */ + confidence?: number; + /** + * The predicted classifier label (e.g. `needs_reasoning`), when available + */ + predictedLabel?: string; + reasoningBucket?: AutoModeResolvedReasoningBucket; +} +/** + * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and where they came from, so SDK clients can show users what is enterprise-managed and by which authority. 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; for a session-independent pull, use the SDK `getManagedSettings()` API, which returns the identical payload. Managed settings have a single authoritative source, so the highest-authority present layer (server > device) wins wholesale; `bypassPermissionsDisabled` is deny-wins across layers. Marked experimental while the managed-settings surface stabilizes. + */ +/** @experimental */ +export interface ManagedSettingsResolvedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: ManagedSettingsResolvedData; + /** + * 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.managed_settings_resolved". + */ + type: "session.managed_settings_resolved"; +} +/** + * Enterprise managed-settings resolution: the effective managed settings the session applied and where they came from, so SDK clients can show users what is enterprise-managed and by which authority. 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; for a session-independent pull, use the SDK `getManagedSettings()` API, which returns the identical payload. Managed settings have a single authoritative source, so the highest-authority present layer (server > device) wins wholesale; `bypassPermissionsDisabled` is deny-wins across layers. Marked experimental while the managed-settings surface stabilizes. + */ +/** @experimental */ +export interface ManagedSettingsResolvedData { + /** + * Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true. + */ + bypassPermissionsDisabled: boolean; + /** + * Whether the device (MDM/plist/registry/file) managed-settings layer was present + */ + deviceManaged: boolean; + /** + * 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. + */ + failClosed: boolean; + /** + * The setting keys under enterprise management in the effective managed settings (e.g. `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force. + */ + managedKeys: string[]; + /** + * Whether the server (account/org) managed-settings layer was present + */ + serverManaged: boolean; + /** + * The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. + */ + settings?: { + [k: string]: unknown | undefined; + }; + source: ManagedSettingsResolvedSource; +} /** * Session event "commands.changed". SDK command registration change notification */ @@ -8244,6 +8579,105 @@ export interface McpServerStatusChangedData { serverName: string; status: McpServerStatus; } +/** + * Session event "mcp.tools.list_changed". Payload identifying the MCP server associated with a list change. + */ +export interface McpToolsListChangedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: McpListChangedData; + /** + * 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 "mcp.tools.list_changed". + */ + type: "mcp.tools.list_changed"; +} +/** + * Payload identifying the MCP server associated with a list change. + */ +export interface McpListChangedData { + /** + * Name of the MCP server whose list changed + */ + serverName: string; +} +/** + * Session event "mcp.resources.list_changed". Payload identifying the MCP server associated with a list change. + */ +export interface McpResourcesListChangedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: McpListChangedData; + /** + * 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 "mcp.resources.list_changed". + */ + type: "mcp.resources.list_changed"; +} +/** + * Session event "mcp.prompts.list_changed". Payload identifying the MCP server associated with a list change. + */ +export interface McpPromptsListChangedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: McpListChangedData; + /** + * 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 "mcp.prompts.list_changed". + */ + type: "mcp.prompts.list_changed"; +} /** * Session event "session.extensions_loaded". Payload of `session.extensions_loaded` listing discovered extensions and their statuses. */ @@ -8299,7 +8733,7 @@ export interface ExtensionsLoadedExtension { status: ExtensionsLoadedExtensionStatus; } /** - * Session event "session.canvas.opened". Payload of `session.canvas.opened` with canvas instance and provider IDs plus optional title, status, URL, and input. + * Session event "session.canvas.opened". Payload of `session.canvas.opened` with canvas instance and provider IDs plus optional icon, title, status, URL, and input. */ /** @experimental */ export interface CanvasOpenedEvent { @@ -8330,7 +8764,7 @@ export interface CanvasOpenedEvent { type: "session.canvas.opened"; } /** - * Payload of `session.canvas.opened` with canvas instance and provider IDs plus optional title, status, URL, and input. + * Payload of `session.canvas.opened` with canvas instance and provider IDs plus optional icon, title, status, URL, and input. */ /** @experimental */ export interface CanvasOpenedData { @@ -8346,6 +8780,10 @@ export interface CanvasOpenedData { * Owning extension display name, when available */ extensionName?: string; + /** + * Host-local PNG path for the canvas icon, when supplied + */ + icon?: string; /** * Input supplied when the instance was opened */ @@ -8439,6 +8877,10 @@ export interface CanvasRegistryChangedCanvas { * Owning extension display name, when available */ extensionName?: string; + /** + * Host-local PNG path for the canvas icon, when supplied + */ + icon?: string; /** * JSON Schema for canvas open input */ diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index e05b33c158..9d3bdcd7f0 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -51,6 +51,7 @@ export type { CommandContext, CommandDefinition, CommandHandler, + CanvasProviderIdentity, CloudSessionOptions, CloudSessionRepository, AutoModeSwitchHandler, @@ -59,8 +60,10 @@ export type { CopilotClientMode, CopilotClientOptions, StdioRuntimeConnection, + InProcessRuntimeConnection, TcpRuntimeConnection, UriRuntimeConnection, + ChildProcessRuntimeConnection, CustomAgentConfig, ElicitationFieldValue, ElicitationHandler, @@ -147,8 +150,10 @@ export type { Tool, ToolHandler, ToolInvocation, + CurrentToolMetadata, ToolTelemetry, ToolResultObject, + ToolSearchConfig, TypedSessionEventHandler, TypedSessionLifecycleHandler, ZodSchema, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 8d8fc6714f..1f71209de8 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -13,6 +13,7 @@ import { createSessionRpc } from "./generated/rpc.js"; import type { ClientSessionApiHandlers, CanvasActionInvokeResult, + CurrentToolMetadata, McpOauthPendingRequestResponse, } from "./generated/rpc.js"; import { type Canvas, CanvasError } from "./canvas.js"; @@ -96,6 +97,8 @@ function isOpenCanvasInstance(value: unknown): value is OpenCanvasInstance { /** Assistant message event - the final response from the assistant. */ export type AssistantMessageEvent = Extract; +const TOOL_SEARCH_TOOL_NAME = "tool_search_tool"; + /** * Represents a single conversation session with the Copilot CLI. * @@ -121,6 +124,12 @@ export type AssistantMessageEvent = Extract = new Set(); private typedEventHandlers: Map void>> = @@ -606,11 +615,26 @@ export class CopilotSession { tracestate?: string ): Promise { try { + // The built-in tool-search tool receives a snapshot of the session's + // currently initialized tools so an override can filter the live + // catalog without issuing its own RPC. Fetch it only for that tool + // to avoid a round-trip on every tool call; a failed fetch simply + // leaves the snapshot undefined rather than failing the tool. + let availableTools: CurrentToolMetadata[] | undefined; + if (toolName === TOOL_SEARCH_TOOL_NAME) { + try { + const metadata = await this.rpc.tools.getCurrentMetadata(); + availableTools = metadata.tools ?? undefined; + } catch { + availableTools = undefined; + } + } const rawResult = await handler(args, { sessionId: this.sessionId, toolCallId, toolName, arguments: args, + availableTools, traceparent, tracestate, }); @@ -1348,7 +1372,7 @@ export class CopilotSession { * * @example * ```typescript - * await session.setModel("gpt-4.1"); + * await session.setModel("gpt-5.4"); * await session.setModel("claude-sonnet-4.6", { reasoningEffort: "high" }); * ``` */ diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 97182b5f1c..fc0fa51d00 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -21,9 +21,11 @@ import type { ModelBillingTokenPrices, OpenCanvasInstance, RemoteSessionMode, + CurrentToolMetadata, } from "./generated/rpc.js"; import type { ToolSet } from "./toolSet.js"; export type { RemoteSessionMode } from "./generated/rpc.js"; +export type { CurrentToolMetadata } from "./generated/rpc.js"; export type { GitHubTelemetryNotification, GitHubTelemetryEvent, @@ -96,25 +98,60 @@ export interface TelemetryConfig { */ export type RuntimeConnection = | StdioRuntimeConnection + | InProcessRuntimeConnection | TcpRuntimeConnection | UriRuntimeConnection; /** - * Spawns a runtime child process and communicates over its stdin/stdout. - * This is the default if no {@link CopilotClientOptions.connection} is set. + * Shared shape for the transports that spawn a runtime **child process** + * ({@link StdioRuntimeConnection} and {@link TcpRuntimeConnection}). */ -export interface StdioRuntimeConnection { - readonly kind: "stdio"; +export interface ChildProcessRuntimeConnection { /** Path to the runtime executable. When omitted, the bundled runtime is used. */ readonly path?: string; /** Extra command-line arguments to pass to the runtime process. */ readonly args?: readonly string[]; + /** + * Environment variables for the spawned runtime child process, replacing the + * inherited environment. Cannot be combined with + * {@link CopilotClientOptions.env}; setting both throws when the client is + * constructed. When omitted, the client-level env (or `process.env`) is used. + */ + readonly env?: Record; +} + +/** + * Spawns a runtime child process and communicates over its stdin/stdout. + * This is the default if no {@link CopilotClientOptions.connection} is set. + */ +export interface StdioRuntimeConnection extends ChildProcessRuntimeConnection { + readonly kind: "stdio"; +} + +/** + * Hosts the runtime in-process by loading the native runtime library and speaking + * JSON-RPC over its C ABI (FFI), instead of spawning a runtime child process. The + * native host spawns the CLI worker itself. Construct via + * {@link RuntimeConnection.forInProcess}. + * + * @experimental The in-process (FFI) transport is experimental and its behavior may + * change. Per-client options that are lowered to environment variables — including + * {@link CopilotClientOptions.env}, {@link CopilotClientOptions.telemetry}, + * {@link CopilotClientOptions.gitHubToken}, and + * {@link CopilotClientOptions.baseDirectory} — are **not** honored with this + * transport, because the native runtime loads into the shared host process and its + * worker inherits that process's ambient environment. To configure the in-process + * runtime, set the corresponding environment variables on the host process before + * constructing the client. See https://github.com/github/copilot-sdk/issues/1934. + */ +export interface InProcessRuntimeConnection { + readonly kind: "inprocess"; } /** * Spawns a runtime child process that listens on a TCP socket and connects to it. */ -export interface TcpRuntimeConnection { +export interface TcpRuntimeConnection extends ChildProcessRuntimeConnection { readonly kind: "tcp"; /** * TCP port to listen on. `0` (the default) auto-allocates a free port. @@ -127,10 +164,6 @@ export interface TcpRuntimeConnection { * loopback listener is safe by default. */ readonly connectionToken?: string; - /** Path to the runtime executable. When omitted, the bundled runtime is used. */ - readonly path?: string; - /** Extra command-line arguments to pass to the runtime process. */ - readonly args?: readonly string[]; } /** @@ -154,8 +187,10 @@ export const RuntimeConnection = { * Spawn a runtime child process and communicate over its stdin/stdout. * This is the default if no {@link CopilotClientOptions.connection} is set. */ - forStdio(opts: { path?: string; args?: readonly string[] } = {}): StdioRuntimeConnection { - return { kind: "stdio", path: opts.path, args: opts.args }; + forStdio( + opts: { path?: string; args?: readonly string[]; env?: Record } = {} + ): StdioRuntimeConnection { + return { kind: "stdio", path: opts.path, args: opts.args, env: opts.env }; }, /** * Spawn a runtime child process that listens on a TCP socket and connect to it. @@ -166,6 +201,7 @@ export const RuntimeConnection = { connectionToken?: string; path?: string; args?: readonly string[]; + env?: Record; } = {} ): TcpRuntimeConnection { return { @@ -174,6 +210,7 @@ export const RuntimeConnection = { connectionToken: opts.connectionToken, path: opts.path, args: opts.args, + env: opts.env, }; }, /** @@ -183,6 +220,18 @@ export const RuntimeConnection = { forUri(url: string, opts: { connectionToken?: string } = {}): UriRuntimeConnection { return { kind: "uri", url, connectionToken: opts.connectionToken }; }, + /** + * Host the runtime in-process over the native runtime library's C ABI (FFI). + * + * @experimental Per-client options lowered to environment variables (`env`, + * `telemetry`, `gitHubToken`, `baseDirectory`) are **not** honored in-process; + * the worker inherits the host process's ambient environment. Set the + * corresponding environment variables on the host process instead. See + * https://github.com/github/copilot-sdk/issues/1934. + */ + forInProcess(): InProcessRuntimeConnection { + return { kind: "inprocess" }; + }, } as const; /** @@ -403,6 +452,10 @@ export type ToolResultObject = { error?: string; sessionLog?: string; toolTelemetry?: ToolTelemetry; + /** + * Names of tools returned by a tool-search tool. + */ + toolReferences?: string[]; }; export type ToolResult = string | ToolResultObject; @@ -527,6 +580,14 @@ export interface ToolInvocation { toolCallId: string; toolName: string; arguments: unknown; + /** + * Snapshot of the session's currently initialized tools. Populated by the + * SDK only when this invocation targets the built-in tool-search tool + * (`tool_search_tool`), so a tool-search override can rank/filter the live + * catalog — including MCP tools configured in settings — without issuing its + * own RPC. `undefined` for every other tool invocation. + */ + availableTools?: CurrentToolMetadata[]; /** W3C Trace Context traceparent from the CLI's execute_tool span. */ traceparent?: string; /** W3C Trace Context tracestate from the CLI's execute_tool span. */ @@ -579,6 +640,14 @@ export interface Tool { * Optional; defaults to `"auto"`. */ defer?: "auto" | "never"; + /** + * Opaque, host-defined metadata associated with the tool definition. + * + * Keys are namespaced and are not part of the stable public API. Values are + * not interpreted and may be recognized to inform host-specific behavior. + * Unknown keys are preserved and round-tripped untouched. + */ + metadata?: Record; } /** @@ -594,11 +663,41 @@ export function defineTool( overridesBuiltInTool?: boolean; skipPermission?: boolean; defer?: "auto" | "never"; + metadata?: Record; } ): Tool { return { name, ...config }; } +/** + * SDK-supplied override for the runtime's built-in tool-search behavior. + * + * Tool search lets the model discover tools on demand instead of loading every + * tool definition up front. When the total tool count exceeds the deferral + * threshold, MCP and external tools are marked as deferred and surfaced through + * the built-in `tool_search_tool`. + * + * To override the tool-search tool's model-facing definition and/or its + * execution, register a {@link Tool} named `tool_search_tool` with + * `overridesBuiltInTool: true`. To customize the in-prompt tool-search + * guidance, use the `tool_instructions` section of {@link SystemMessageConfig} + * in `"customize"` mode. + */ +export interface ToolSearchConfig { + /** + * Toggle to enable/disable tool search. When disabled, all tools are pre-loaded + * and the model's active tool set is not deferred. + */ + enabled?: boolean; + + /** + * Overrides the total tool count at which MCP and external tools are + * automatically deferred behind tool search. Defaults to the built-in + * threshold (30) when omitted. + */ + deferThreshold?: number; +} + // ============================================================================ // Commands // ============================================================================ @@ -1542,6 +1641,12 @@ export interface CustomAgentConfig { * falling back to the parent session model if unavailable. */ model?: string; + /** + * Reasoning effort level for this agent's model. + * When omitted, no per-agent override is sent and the backend chooses its + * default. The parent session effort is not inherited. + */ + reasoningEffort?: ReasoningEffort; } /** @@ -1714,6 +1819,23 @@ export interface ExtensionInfo { name: string; } +/** + * Stable identity for a host/SDK connection that supplies built-in canvases. + * + * When set on session create or resume, the runtime uses {@link id} verbatim + * as the agent-facing canvas extension id, so canvases declared on a control + * connection survive stdio reconnect and CLI process restart instead of being + * re-keyed to a per-connection id. The id is opaque to the runtime; a + * per-window-stable value such as `app:builtin:` is recommended. An + * id beginning with `connection:` is reserved and ignored by the runtime. + */ +export interface CanvasProviderIdentity { + /** Opaque, stable provider id used verbatim as the canvas extension id. */ + id: string; + /** Optional display name surfaced as the canvas extension name. */ + name?: string; +} + /** * Provider-scoped options for the Copilot API (CAPI). * @@ -1858,6 +1980,14 @@ export interface SessionConfigBase { */ extensionInfo?: ExtensionInfo; + /** + * Stable identity for a host/SDK connection that supplies built-in + * canvases. When set, the runtime uses `id` verbatim as the agent-facing + * canvas extension id, so canvases declared on a control connection survive + * reconnect and CLI restart. Honored on session create and resume. + */ + canvasProvider?: CanvasProviderIdentity; + /** * Slash commands registered for this session. * When the CLI has a TUI, each command appears as `/name` for the user to invoke. @@ -1871,6 +2001,15 @@ export interface SessionConfigBase { */ systemMessage?: SystemMessageConfig; + /** + * Override for the runtime's built-in tool-search behavior. + * + * To also override the tool-search tool's implementation, register a + * {@link Tool} named `tool_search_tool` with `overridesBuiltInTool: true` in + * {@link SessionConfigBase.tools}. + */ + toolSearch?: ToolSearchConfig; + /** * List of tool names to allow. When specified, only these tools will be available. * @@ -2189,6 +2328,14 @@ export interface SessionConfigBase { */ gitHubToken?: string; + /** + * Opt-in: when true, the runtime self-fetches enterprise managed settings + * (bypass-permissions policy) at session bootstrap using the session's + * `gitHubToken`. Requires {@link SessionConfigBase.gitHubToken} to be set; + * if omitted, the runtime is expected to reject session creation (fail-closed). + */ + enableManagedSettings?: boolean; + /** * When true, skips embedding-based retrieval for this session. * Use in multitenant deployments to prevent cross-session information leakage diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 96c32a5951..e90143cb44 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { EventEmitter } from "node:events"; -import { describe, expect, it, onTestFinished, vi } from "vitest"; import { PassThrough } from "stream"; +import { describe, expect, it, onTestFinished, vi } from "vitest"; import { approveAll, CopilotClient, @@ -15,6 +15,10 @@ import { defaultJoinSessionPermissionHandler } from "../src/types.js"; // This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.test.ts instead +async function stopClient(client: CopilotClient): Promise { + await client.stop(); +} + describe("CopilotClient", () => { it("disposes the stdio connection when child stdin emits an error", async () => { const client = new CopilotClient(); @@ -128,7 +132,7 @@ describe("CopilotClient", () => { it("registers interest in MCP OAuth required events after create when an auth handler is configured", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -156,7 +160,7 @@ describe("CopilotClient", () => { it("does not register MCP OAuth interest without an auth handler", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -183,7 +187,7 @@ describe("CopilotClient", () => { it("registers MCP OAuth interest after cloud create only when an auth handler is configured", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); let cloudCreateCount = 0; const spy = vi @@ -224,7 +228,7 @@ describe("CopilotClient", () => { it("registers MCP OAuth interest after resuming only when an auth handler is configured", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -278,7 +282,7 @@ describe("CopilotClient", () => { it("forwards canvas declarations and request flags in session.create", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const canvas = createCanvas({ id: "counter", @@ -301,6 +305,7 @@ describe("CopilotClient", () => { requestCanvasRenderer: true, requestExtensions: true, extensionInfo: { source: "github-app", name: "counter-provider" }, + canvasProvider: { id: "app:builtin:window-1", name: "Built-in" }, }); const payload = spy.mock.calls.find(([method]) => method === "session.create")![1] as any; @@ -318,12 +323,16 @@ describe("CopilotClient", () => { source: "github-app", name: "counter-provider", }); + expect(payload.canvasProvider).toEqual({ + id: "app:builtin:window-1", + name: "Built-in", + }); }); it("forwards canvas declarations in session.resume", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const canvas = createCanvas({ @@ -345,6 +354,7 @@ describe("CopilotClient", () => { requestCanvasRenderer: true, requestExtensions: true, extensionInfo: { source: "github-app", name: "counter-provider" }, + canvasProvider: { id: "app:builtin:window-1" }, }); const payload = spy.mock.calls.find(([method]) => method === "session.resume")![1] as any; @@ -355,13 +365,14 @@ describe("CopilotClient", () => { source: "github-app", name: "counter-provider", }); + expect(payload.canvasProvider).toEqual({ id: "app:builtin:window-1" }); expect(payload.openCanvasInstances).toBeUndefined(); }); it("forwards reasoningSummary in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -393,7 +404,7 @@ describe("CopilotClient", () => { it("forwards contextTier in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -422,11 +433,76 @@ describe("CopilotClient", () => { expect(resumePayload.contextTier).toBe("default"); }); - it("forwards new session options in session.create and session.resume", async () => { + it("forwards tool metadata verbatim in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start(); onTestFinished(() => client.forceStop()); + 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 metadata = { + "github.com/copilot:safeForTelemetry": { name: true, inputsNames: false }, + }; + const tool = { + name: "my_tool", + description: "a tool", + parameters: { type: "object", properties: {} }, + metadata, + }; + + const session = await client.createSession({ + onPermissionRequest: approveAll, + tools: [tool], + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + tools: [tool], + }); + + 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.tools[0].metadata).toEqual(metadata); + expect(resumePayload.tools[0].metadata).toEqual(metadata); + }); + + it("omits tool metadata from session.create when unset", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + await client.createSession({ + onPermissionRequest: approveAll, + tools: [{ name: "my_tool", description: "a tool" }], + }); + + const createPayload = spy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + expect(createPayload.tools[0].metadata).toBeUndefined(); + }); + + it("forwards new session options in session.create and session.resume", async () => { + 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) => { @@ -465,7 +541,7 @@ describe("CopilotClient", () => { it("opts into GitHub telemetry forwarding when onGitHubTelemetry is provided", async () => { const client = new CopilotClient({ onGitHubTelemetry: () => {} }); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -490,7 +566,7 @@ describe("CopilotClient", () => { it("opts into GitHub telemetry forwarding on the connect handshake when a handler is provided", async () => { const client = new CopilotClient({ onGitHubTelemetry: () => {} }); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const sendRequest = vi.fn(async (method: string) => { if (method === "connect") return { ok: true, protocolVersion: 3, version: "test" }; @@ -507,7 +583,7 @@ describe("CopilotClient", () => { it("does not opt into GitHub telemetry forwarding on the connect handshake without a handler", async () => { const client = new CopilotClient(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const sendRequest = vi.fn(async (method: string) => { if (method === "connect") return { ok: true, protocolVersion: 3, version: "test" }; @@ -525,7 +601,7 @@ describe("CopilotClient", () => { it("does not opt into GitHub telemetry forwarding without a handler", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -604,7 +680,7 @@ describe("CopilotClient", () => { it("registers no gitHubTelemetry handler when onGitHubTelemetry is omitted", () => { const client = new CopilotClient(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const handlers = (client as any).clientGlobalHandlers; expect(handlers.gitHubTelemetry).toBeUndefined(); @@ -613,7 +689,7 @@ describe("CopilotClient", () => { it("forwards gitHubTelemetry events to the onGitHubTelemetry handler", () => { const received: GitHubTelemetryNotification[] = []; const client = new CopilotClient({ onGitHubTelemetry: (n) => received.push(n) }); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const handlers = (client as any).clientGlobalHandlers; expect(handlers.gitHubTelemetry).toBeDefined(); @@ -630,7 +706,7 @@ describe("CopilotClient", () => { it("forwards expAssignments in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -667,7 +743,7 @@ describe("CopilotClient", () => { it("omits expAssignments from session.create and session.resume when unset", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -693,7 +769,7 @@ describe("CopilotClient", () => { it("forwards capi options in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -725,7 +801,7 @@ describe("CopilotClient", () => { it("forwards pluginDirectories and largeOutput in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -966,7 +1042,7 @@ describe("CopilotClient", () => { it("forwards clientName in session.create request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi.spyOn((client as any).connection!, "sendRequest"); await client.createSession({ clientName: "my-app", onPermissionRequest: approveAll }); @@ -980,7 +1056,7 @@ describe("CopilotClient", () => { it("forwards cloud options in session.create request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -1005,7 +1081,7 @@ describe("CopilotClient", () => { it("forwards clientName in session.resume request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); // Mock sendRequest to capture the call without hitting the runtime @@ -1030,7 +1106,7 @@ describe("CopilotClient", () => { it("forwards enableSessionTelemetry in session.create request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi.spyOn((client as any).connection!, "sendRequest"); await client.createSession({ @@ -1047,7 +1123,7 @@ describe("CopilotClient", () => { it("forwards enableSessionTelemetry in session.resume request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -1071,7 +1147,7 @@ describe("CopilotClient", () => { it("forwards enableOnDemandInstructionDiscovery in session.create request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi.spyOn((client as any).connection!, "sendRequest"); await client.createSession({ @@ -1088,7 +1164,7 @@ describe("CopilotClient", () => { it("forwards enableOnDemandInstructionDiscovery in session.resume request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -1115,7 +1191,7 @@ describe("CopilotClient", () => { it("defaults includeSubAgentStreamingEvents to true in session.create when not specified", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi.spyOn((client as any).connection!, "sendRequest"); await client.createSession({ onPermissionRequest: approveAll }); @@ -1127,7 +1203,7 @@ describe("CopilotClient", () => { it("forwards explicit false for includeSubAgentStreamingEvents in session.create", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi.spyOn((client as any).connection!, "sendRequest"); await client.createSession({ @@ -1142,7 +1218,7 @@ describe("CopilotClient", () => { it("defaults includeSubAgentStreamingEvents to true in session.resume when not specified", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -1161,7 +1237,7 @@ describe("CopilotClient", () => { it("forwards explicit false for includeSubAgentStreamingEvents in session.resume", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -1183,7 +1259,7 @@ describe("CopilotClient", () => { it("defaults mcpOAuthTokenStorage to 'in-memory' in session.create when mode is empty", async () => { const client = new CopilotClient({ mode: "empty", baseDirectory: "/tmp/copilot-test" }); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -1201,7 +1277,7 @@ describe("CopilotClient", () => { it("does not send mcpOAuthTokenStorage in session.create when mode is copilot-cli", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi.spyOn((client as any).connection!, "sendRequest"); await client.createSession({ onPermissionRequest: approveAll }); @@ -1213,7 +1289,7 @@ describe("CopilotClient", () => { it("forwards explicit 'persistent' for mcpOAuthTokenStorage in session.create", async () => { const client = new CopilotClient({ mode: "empty", baseDirectory: "/tmp/copilot-test" }); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -1235,7 +1311,7 @@ describe("CopilotClient", () => { it("defaults mcpOAuthTokenStorage to 'in-memory' in session.resume when mode is empty", async () => { const client = new CopilotClient({ mode: "empty", baseDirectory: "/tmp/copilot-test" }); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -1255,7 +1331,7 @@ describe("CopilotClient", () => { it("forwards explicit 'persistent' for mcpOAuthTokenStorage in session.resume", async () => { const client = new CopilotClient({ mode: "empty", baseDirectory: "/tmp/copilot-test" }); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -1279,7 +1355,7 @@ describe("CopilotClient", () => { it("defaults memory to { enabled: false } in session.create when mode is empty", async () => { const client = new CopilotClient({ mode: "empty", baseDirectory: "/tmp/copilot-test" }); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -1297,7 +1373,7 @@ describe("CopilotClient", () => { it("does not send memory in session.create when mode is copilot-cli", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi.spyOn((client as any).connection!, "sendRequest"); await client.createSession({ onPermissionRequest: approveAll }); @@ -1309,7 +1385,7 @@ describe("CopilotClient", () => { it("forwards explicit memory config in session.create even in empty mode", async () => { const client = new CopilotClient({ mode: "empty", baseDirectory: "/tmp/copilot-test" }); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -1331,7 +1407,7 @@ describe("CopilotClient", () => { it("defaults memory to { enabled: false } in session.resume when mode is empty", async () => { const client = new CopilotClient({ mode: "empty", baseDirectory: "/tmp/copilot-test" }); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -1351,7 +1427,7 @@ describe("CopilotClient", () => { it("does not send memory in session.resume when mode is copilot-cli", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -1370,7 +1446,7 @@ describe("CopilotClient", () => { it("forwards continuePendingWork in session.resume request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -1392,7 +1468,7 @@ describe("CopilotClient", () => { it("omits continuePendingWork from session.resume payload when not specified", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -1411,7 +1487,7 @@ describe("CopilotClient", () => { it("forwards memory configuration in session.create request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -1434,7 +1510,7 @@ describe("CopilotClient", () => { it("forwards memory configuration in session.resume request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -1456,7 +1532,7 @@ describe("CopilotClient", () => { it("omits memory from session.create payload when not specified", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -1477,7 +1553,7 @@ describe("CopilotClient", () => { it("forwards provider headers in session.create request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi .spyOn((client as any).connection!, "sendRequest") @@ -1518,7 +1594,7 @@ describe("CopilotClient", () => { it("forwards provider headers in session.resume request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -1559,7 +1635,7 @@ describe("CopilotClient", () => { it("forwards defaultAgent in session.create request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi.spyOn((client as any).connection!, "sendRequest"); await client.createSession({ @@ -1578,7 +1654,7 @@ describe("CopilotClient", () => { it("forwards defaultAgent in session.resume request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi.spyOn((client as any).connection!, "sendRequest"); @@ -1598,7 +1674,7 @@ describe("CopilotClient", () => { it("forwards instructionDirectories in session.create request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const instructionDirectories = ["C:\\extra-instructions", "C:\\more-instructions"]; const spy = vi.spyOn((client as any).connection!, "sendRequest"); @@ -1616,7 +1692,7 @@ describe("CopilotClient", () => { it("forwards instructionDirectories in session.resume request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const instructionDirectories = ["C:\\resume-instructions"]; @@ -1644,7 +1720,7 @@ describe("CopilotClient", () => { it("does not request permissions on session.resume when using the default joinSession handler", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -1671,7 +1747,7 @@ describe("CopilotClient", () => { it("requests permissions on session.resume when using an explicit handler", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -1698,7 +1774,7 @@ describe("CopilotClient", () => { it("forwards mode callback request flags in session.resume request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -1728,7 +1804,7 @@ describe("CopilotClient", () => { it("sends session.model.switchTo RPC with correct params", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); @@ -1754,7 +1830,7 @@ describe("CopilotClient", () => { it("sends reasoning options with session.model.switchTo when provided", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); @@ -1996,13 +2072,84 @@ describe("CopilotClient", () => { /gitHubToken and useLoggedInUser cannot be used with RuntimeConnection.forUri/ ); }); + + it("should throw error when env is used with forInProcess", () => { + expect(() => { + new CopilotClient({ + connection: RuntimeConnection.forInProcess(), + env: { FOO: "bar" }, + logLevel: "error", + }); + }).toThrow(/env is not supported with RuntimeConnection.forInProcess/); + }); + + it("should throw error when telemetry is used with forInProcess", () => { + expect(() => { + new CopilotClient({ + connection: RuntimeConnection.forInProcess(), + telemetry: { otlpEndpoint: "http://localhost:4318" }, + logLevel: "error", + }); + }).toThrow(/telemetry is not supported with RuntimeConnection.forInProcess/); + }); + + it("should throw error when workingDirectory is used with forInProcess", () => { + expect(() => { + new CopilotClient({ + connection: RuntimeConnection.forInProcess(), + workingDirectory: "/tmp", + logLevel: "error", + }); + }).toThrow(/workingDirectory is not supported with RuntimeConnection.forInProcess/); + }); + + it("should throw error when env is set on both the client and a stdio connection", () => { + expect(() => { + new CopilotClient({ + connection: RuntimeConnection.forStdio({ env: { FOO: "conn" } }), + env: { FOO: "client" }, + logLevel: "error", + }); + }).toThrow( + /Set environment variables via either the client-level env option or the connection/ + ); + }); + + it("should throw error when env is set on both the client and a tcp connection", () => { + expect(() => { + new CopilotClient({ + connection: RuntimeConnection.forTcp({ env: { FOO: "conn" } }), + env: { FOO: "client" }, + logLevel: "error", + }); + }).toThrow( + /Set environment variables via either the client-level env option or the connection/ + ); + }); + + it("should use the connection-level env for child-process transports", () => { + const client = new CopilotClient({ + connection: RuntimeConnection.forStdio({ env: { FOO: "from-conn" } }), + logLevel: "error", + }); + expect((client as any).resolvedEnv).toEqual({ FOO: "from-conn" }); + }); + + it("should allow env on the client alone with a child-process transport", () => { + const client = new CopilotClient({ + connection: RuntimeConnection.forStdio(), + env: { FOO: "from-client" }, + logLevel: "error", + }); + expect((client as any).resolvedEnv).toEqual({ FOO: "from-client" }); + }); }); describe("overridesBuiltInTool in tool definitions", () => { it("sends overridesBuiltInTool in tool definition on session.create", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi.spyOn((client as any).connection!, "sendRequest"); await client.createSession({ @@ -2026,7 +2173,7 @@ describe("CopilotClient", () => { it("sends overridesBuiltInTool in tool definition on session.resume", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); // Mock sendRequest to capture the call without hitting the runtime @@ -2060,7 +2207,7 @@ describe("CopilotClient", () => { it("sends defer in tool definition on session.create", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi.spyOn((client as any).connection!, "sendRequest"); await client.createSession({ @@ -2084,7 +2231,7 @@ describe("CopilotClient", () => { it("sends defer in tool definition on session.resume", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -2117,7 +2264,7 @@ describe("CopilotClient", () => { it("forwards agent in session.create request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi.spyOn((client as any).connection!, "sendRequest"); await client.createSession({ @@ -2134,12 +2281,13 @@ describe("CopilotClient", () => { const payload = spy.mock.calls.find((c) => c[0] === "session.create")![1] as any; expect(payload.agent).toBe("test-agent"); expect(payload.customAgents).toEqual([expect.objectContaining({ name: "test-agent" })]); + expect(payload.customAgents[0].reasoningEffort).toBeUndefined(); }); - it("forwards custom agent model in session.create request", async () => { + it("forwards custom agent model and reasoning effort in session.create request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi.spyOn((client as any).connection!, "sendRequest"); await client.createSession({ @@ -2149,20 +2297,25 @@ describe("CopilotClient", () => { name: "model-agent", prompt: "You are a model agent.", model: "claude-haiku-4.5", + reasoningEffort: "high", }, ], }); const payload = spy.mock.calls.find((c) => c[0] === "session.create")![1] as any; expect(payload.customAgents).toEqual([ - expect.objectContaining({ name: "model-agent", model: "claude-haiku-4.5" }), + expect.objectContaining({ + name: "model-agent", + model: "claude-haiku-4.5", + reasoningEffort: "high", + }), ]); }); it("forwards agent in session.resume request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -2220,7 +2373,7 @@ describe("CopilotClient", () => { const handler = vi.fn().mockReturnValue(customModels); const client = new CopilotClient({ onListModels: handler }); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const models = await client.listModels(); expect(handler).toHaveBeenCalledTimes(1); @@ -2243,7 +2396,7 @@ describe("CopilotClient", () => { const handler = vi.fn().mockReturnValue(customModels); const client = new CopilotClient({ onListModels: handler }); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); await client.listModels(); await client.listModels(); @@ -2265,7 +2418,7 @@ describe("CopilotClient", () => { const handler = vi.fn().mockResolvedValue(customModels); const client = new CopilotClient({ onListModels: handler }); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const models = await client.listModels(); expect(models).toEqual(customModels); @@ -2293,22 +2446,29 @@ describe("CopilotClient", () => { }); describe("unexpected disconnection", () => { - it("transitions to disconnected when child process is killed", async () => { - const client = new CopilotClient(); - await client.start(); - onTestFinished(() => client.forceStop()); - - expect((client as any).state).toBe("connected"); - - // Kill the child process to simulate unexpected termination - const proc = (client as any).cliProcess as import("node:child_process").ChildProcess; - proc.kill(); - - // Wait for the connection.onClose handler to fire - await vi.waitFor(() => { - expect((client as any).state).toBe("disconnected"); - }); - }); + // No child process exists over the in-process (FFI) transport, so this + // child-process-kill scenario does not apply there. Covered by the default + // (stdio) cell. + it.skipIf((process.env.COPILOT_SDK_DEFAULT_CONNECTION ?? "").toLowerCase() === "inprocess")( + "transitions to disconnected when child process is killed", + async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + expect((client as any).state).toBe("connected"); + + // Kill the child process to simulate unexpected termination + const proc = (client as any) + .cliProcess as import("node:child_process").ChildProcess; + proc.kill(); + + // Wait for the connection.onClose handler to fire + await vi.waitFor(() => { + expect((client as any).state).toBe("disconnected"); + }); + } + ); }); describe("onGetTraceContext", () => { @@ -2320,7 +2480,7 @@ describe("CopilotClient", () => { const provider = vi.fn().mockReturnValue(traceContext); const client = new CopilotClient({ onGetTraceContext: provider }); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi.spyOn((client as any).connection!, "sendRequest"); await client.createSession({ onPermissionRequest: approveAll }); @@ -2342,7 +2502,7 @@ describe("CopilotClient", () => { const provider = vi.fn().mockReturnValue(traceContext); const client = new CopilotClient({ onGetTraceContext: provider }); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -2368,7 +2528,7 @@ describe("CopilotClient", () => { const provider = vi.fn().mockReturnValue(traceContext); const client = new CopilotClient({ onGetTraceContext: provider }); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -2390,7 +2550,7 @@ describe("CopilotClient", () => { it("forwards requestHeaders in session.send request", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -2417,7 +2577,7 @@ describe("CopilotClient", () => { it("does not include trace context when no callback is provided", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi.spyOn((client as any).connection!, "sendRequest"); await client.createSession({ onPermissionRequest: approveAll }); @@ -2432,7 +2592,7 @@ describe("CopilotClient", () => { it("forwards commands in session.create RPC", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const spy = vi.spyOn((client as any).connection!, "sendRequest"); await client.createSession({ @@ -2453,7 +2613,7 @@ describe("CopilotClient", () => { it("forwards commands in session.resume RPC", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi @@ -2475,7 +2635,7 @@ describe("CopilotClient", () => { it("routes command.execute event to the correct handler", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const handler = vi.fn(); const session = await client.createSession({ @@ -2529,7 +2689,7 @@ describe("CopilotClient", () => { it("sends error when command handler throws", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll, @@ -2577,7 +2737,7 @@ describe("CopilotClient", () => { it("sends error for unknown command", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll, @@ -2623,7 +2783,7 @@ describe("CopilotClient", () => { it("reads capabilities from session.create response", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); // Intercept session.create to inject capabilities const origSendRequest = (client as any).connection!.sendRequest.bind( @@ -2649,7 +2809,7 @@ describe("CopilotClient", () => { it("defaults capabilities when not injected", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); // CLI returns actual capabilities (elicitation false in headless mode) @@ -2659,7 +2819,7 @@ describe("CopilotClient", () => { it("elicitation throws when capability is missing", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll }); @@ -2678,7 +2838,7 @@ describe("CopilotClient", () => { it("sends requestElicitation flag when onElicitationRequest is provided", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const rpcSpy = vi.spyOn((client as any).connection!, "sendRequest"); @@ -2704,7 +2864,7 @@ describe("CopilotClient", () => { it("does not send requestElicitation when no handler provided", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const rpcSpy = vi.spyOn((client as any).connection!, "sendRequest"); @@ -2726,7 +2886,7 @@ describe("CopilotClient", () => { it("sends mode callback request flags based on handler presence", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const rpcSpy = vi.spyOn((client as any).connection!, "sendRequest"); @@ -2761,7 +2921,7 @@ describe("CopilotClient", () => { it("dispatches mode callback requests to registered handlers", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll, @@ -2809,7 +2969,7 @@ describe("CopilotClient", () => { it("sends cancel when elicitation handler throws", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const session = await client.createSession({ onPermissionRequest: approveAll, @@ -2869,7 +3029,7 @@ describe("CopilotClient", () => { it("dispatches postToolUseFailure to onPostToolUseFailure handler", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const received: { input: any; invocation: any }[] = []; const session = await client.createSession({ @@ -2910,7 +3070,7 @@ describe("CopilotClient", () => { it("does not fall back to onPostToolUse for postToolUseFailure events", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const postUseCalls: string[] = []; const session = await client.createSession({ @@ -2939,7 +3099,7 @@ describe("CopilotClient", () => { it("dispatches postToolUse and postToolUseFailure to their respective handlers", async () => { const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const postCalls: string[] = []; const failureCalls: string[] = []; @@ -2979,7 +3139,7 @@ describe("CopilotClient", () => { it("routes hooks.invoke JSON-RPC requests to the SessionHooks handler", async () => { // Validates the full JSON-RPC entry point used by the CLI: - // CopilotClient.handleHooksInvoke({sessionId, hookType, input}) + // clientGlobalHandlers.hooks.invoke({sessionId, hookType, input}) // → CopilotSession._handleHooksInvoke(hookType, input) // → SessionHooks.onPostToolUseFailure(normalizedInput, {sessionId}) // @@ -2989,7 +3149,7 @@ describe("CopilotClient", () => { // The SDK maps that to public `{..., timestamp: Date, workingDirectory}`. const client = new CopilotClient(); await client.start(); - onTestFinished(() => client.forceStop()); + onTestFinished(() => stopClient(client)); const received: { input: any; invocation: any }[] = []; const session = await client.createSession({ @@ -3010,7 +3170,7 @@ describe("CopilotClient", () => { cwd: "/tmp", }; - const response = await (client as any).handleHooksInvoke({ + const response = await (client as any).clientGlobalHandlers.hooks.invoke({ sessionId: session.sessionId, hookType: "postToolUseFailure", input: failureInput, diff --git a/nodejs/test/e2e/client.e2e.test.ts b/nodejs/test/e2e/client.e2e.test.ts index 33b7a0636b..89489f78e6 100644 --- a/nodejs/test/e2e/client.e2e.test.ts +++ b/nodejs/test/e2e/client.e2e.test.ts @@ -1,11 +1,12 @@ import { ChildProcess } from "child_process"; import { describe, expect, it, onTestFinished } from "vitest"; -import { CopilotClient, approveAll, RuntimeConnection } from "../../src/index.js"; +import { approveAll, CopilotClient, RuntimeConnection } from "../../src/index.js"; +import { isInProcessTransport } from "./harness/sdkTestContext.js"; -function onTestFinishedForceStop(client: CopilotClient) { +function onTestFinishedStop(client: CopilotClient) { onTestFinished(async () => { try { - await client.forceStop(); + await client.stop(); } catch { // Ignore cleanup errors - process may already be stopped } @@ -18,7 +19,7 @@ describe("Client", () => { { transport: "tcp", connection: () => RuntimeConnection.forTcp() }, ])("allows createSession without onPermissionRequest ($transport)", async ({ connection }) => { const client = new CopilotClient({ connection: connection() }); - onTestFinishedForceStop(client); + onTestFinishedStop(client); await using session = await client.createSession({}); expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); @@ -30,7 +31,7 @@ describe("Client", () => { const client = new CopilotClient({ connection: RuntimeConnection.forTcp({ connectionToken }), }); - onTestFinishedForceStop(client); + onTestFinishedStop(client); await using originalSession = await client.createSession({}); @@ -42,7 +43,7 @@ describe("Client", () => { const resumeClient = new CopilotClient({ connection: RuntimeConnection.forUri(`localhost:${port}`, { connectionToken }), }); - onTestFinishedForceStop(resumeClient); + onTestFinishedStop(resumeClient); await using resumedSession = await resumeClient.resumeSession( originalSession.sessionId, @@ -53,7 +54,7 @@ describe("Client", () => { it("should start and connect to server using stdio", async () => { const client = new CopilotClient(); - onTestFinishedForceStop(client); + onTestFinishedStop(client); await client.start(); @@ -66,7 +67,7 @@ describe("Client", () => { it("should start and connect to server using tcp", async () => { const client = new CopilotClient({ connection: RuntimeConnection.forTcp() }); - onTestFinishedForceStop(client); + onTestFinishedStop(client); await client.start(); @@ -106,9 +107,14 @@ describe("Client", () => { 60_000 ); - it("should forceStop without cleanup", async () => { + // Skipping on in-proc: + // - It breaks the macOS E2E run (failure: EPIPE) + // - It's not clear that anyone should use forceStop in the in-proc case - there's no child process + // to terminate, so we can't be sure to leave a clean state + // - If you want to get to a clean state within your process, that's what "stop" (not "forceStop") is for + it.skipIf(isInProcessTransport)("should forceStop without cleanup", async () => { const client = new CopilotClient({}); - onTestFinishedForceStop(client); + onTestFinishedStop(client); await client.createSession({ onPermissionRequest: approveAll }); await client.forceStop(); @@ -116,7 +122,7 @@ describe("Client", () => { it("should get status with version and protocol info", async () => { const client = new CopilotClient(); - onTestFinishedForceStop(client); + onTestFinishedStop(client); await client.start(); @@ -132,7 +138,7 @@ describe("Client", () => { it("should get auth status", async () => { const client = new CopilotClient(); - onTestFinishedForceStop(client); + onTestFinishedStop(client); await client.start(); @@ -148,7 +154,7 @@ describe("Client", () => { it("should list models when authenticated", async () => { const client = new CopilotClient(); - onTestFinishedForceStop(client); + onTestFinishedStop(client); await client.start(); @@ -177,7 +183,7 @@ describe("Client", () => { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ args: ["--nonexistent-flag-for-testing"] }), }); - onTestFinishedForceStop(client); + onTestFinishedStop(client); let initialError: Error | undefined; try { diff --git a/nodejs/test/e2e/client_api.e2e.test.ts b/nodejs/test/e2e/client_api.e2e.test.ts index 4adaad6ec3..46c23cee69 100644 --- a/nodejs/test/e2e/client_api.e2e.test.ts +++ b/nodejs/test/e2e/client_api.e2e.test.ts @@ -44,6 +44,7 @@ describe("Client session management", async () => { await waitFor(async () => (await client.listSessions()).some((s) => s.sessionId === sessionId) ); + await session.abort(); await session.disconnect(); await client.deleteSession(sessionId); diff --git a/nodejs/test/e2e/client_options.e2e.test.ts b/nodejs/test/e2e/client_options.e2e.test.ts index 2910b6fb9c..e207f275ab 100644 --- a/nodejs/test/e2e/client_options.e2e.test.ts +++ b/nodejs/test/e2e/client_options.e2e.test.ts @@ -182,7 +182,7 @@ describe("Client options", async () => { }); onTestFinished(async () => { try { - await client.forceStop(); + await client.stop(); } catch { // Ignore cleanup errors } @@ -206,7 +206,7 @@ describe("Client options", async () => { }); onTestFinished(async () => { try { - await client.forceStop(); + await client.stop(); } catch { // Ignore cleanup errors } @@ -237,7 +237,7 @@ describe("Client options", async () => { }); onTestFinished(async () => { try { - await client.forceStop(); + await client.stop(); } catch { // Ignore cleanup errors } @@ -291,7 +291,7 @@ describe("Client options", async () => { }); onTestFinished(async () => { try { - await client.forceStop(); + await client.stop(); } catch { // Ignore cleanup errors } @@ -374,7 +374,7 @@ describe("Client options", async () => { }); onTestFinished(async () => { try { - await client.forceStop(); + await client.stop(); } catch { // Ignore cleanup errors } @@ -523,7 +523,7 @@ describe("Client options", async () => { }); onTestFinished(async () => { try { - await client.forceStop(); + await client.stop(); } catch { // Ignore cleanup errors } @@ -590,7 +590,7 @@ describe("Client options", async () => { }); onTestFinished(async () => { try { - await client.forceStop(); + await client.stop(); } catch { // Ignore cleanup errors } 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 5a9cad5a59..69bacd4f6e 100644 --- a/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts +++ b/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; import { approveAll, CopilotRequestHandler, type CopilotRequestContext } from "../../src/index.js"; -import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { createSdkTestContext, isInProcessTransport } from "./harness/sdkTestContext.js"; /** * Cancellation and error coverage for {@link CopilotRequestHandler}. These two @@ -162,23 +162,33 @@ describe("CopilotRequestHandler observes runtime cancellation", async () => { copilotClientOptions: { requestHandler: handler }, }); - it("fires ctx.signal when the consumer aborts an in-flight inference request", async () => { - await client.start(); - const session = await client.createSession({ onPermissionRequest: approveAll }); - try { - await session.send("Say OK."); - await waitFor(() => handler.inferenceEntered, 60_000); - await session.abort(); - await waitFor(() => handler.sawAbort, 30_000); - } finally { - await session.disconnect(); - } + // The runtime enforces a single, process-wide LLM inference provider: a second + // client.start() with a requestHandler rejects llmInference.setProvider with + // "Another client is already the LLM inference provider." The sibling error test + // above already registers a provider and holds it for this file's lifetime, and + // inproc runs share one runtime host, so this scenario can only run on the default + // (stdio) cell, where each client owns its own runtime process. + it.skipIf(isInProcessTransport)( + "fires ctx.signal when the consumer aborts an in-flight inference request", + async () => { + await client.start(); + const session = await client.createSession({ onPermissionRequest: approveAll }); + try { + await session.send("Say OK."); + await waitFor(() => handler.inferenceEntered, 60_000); + await session.abort(); + await waitFor(() => handler.sawAbort, 30_000); + } finally { + await session.disconnect(); + } - expect(handler.inferenceEntered, "expected the inference callback to be entered").toBe( - true - ); - expect(handler.sawAbort, "expected the callback to observe runtime cancellation").toBe( - true - ); - }, 90_000); + expect(handler.inferenceEntered, "expected the inference callback to be entered").toBe( + true + ); + expect(handler.sawAbort, "expected the callback to observe runtime cancellation").toBe( + true + ); + }, + 90_000 + ); }); 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 3f01475aae..bd070c20ca 100644 --- a/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts +++ b/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts @@ -11,6 +11,9 @@ const SYNTHETIC_TEXT = "OK from the synthetic stream."; interface InterceptedRequest { url: string; sessionId?: string; + agentId?: string; + parentAgentId?: string; + interactionType?: string; } function isInferenceUrl(url: string): boolean { @@ -43,7 +46,13 @@ class RecordingRequestHandler extends CopilotRequestHandler { ctx: CopilotRequestContext ): Promise { const url = request.url; - this.records.push({ url, sessionId: ctx.sessionId }); + this.records.push({ + url, + sessionId: ctx.sessionId, + agentId: ctx.agentId, + parentAgentId: ctx.parentAgentId, + interactionType: ctx.interactionType, + }); const bodyText = request.body ? await request.text() : ""; return isInferenceUrl(url) ? buildInferenceResponse(url, bodyText) @@ -105,6 +114,11 @@ function buildNonInferenceResponse(url: string): Response { return json("{}"); } +function expectAgentMetadata(r: InterceptedRequest): void { + expect(r.agentId).toBeTruthy(); + expect(r.interactionType).toBeTruthy(); +} + const RESPONSES_STREAM_EVENTS: string[] = [ `event: response.created\ndata: ${JSON.stringify({ type: "response.created", @@ -273,6 +287,7 @@ describe("CopilotRequestHandler threads the runtime session id (CAPI + BYOK)", a expect(r.sessionId, "CAPI inference request must carry the runtime session id").toBe( session.sessionId ); + expectAgentMetadata(r); } // Validate the final assistant response arrived (guards against truncated captures) @@ -313,6 +328,7 @@ describe("CopilotRequestHandler threads the runtime session id (CAPI + BYOK)", a expect(r.sessionId, "BYOK inference request must carry the runtime session id").toBe( byokSessionId ); + expectAgentMetadata(r); } // Session ids are per-session, so the two turns must differ — proves diff --git a/nodejs/test/e2e/harness/sdkTestContext.ts b/nodejs/test/e2e/harness/sdkTestContext.ts index a59f62126d..bf62db4826 100644 --- a/nodejs/test/e2e/harness/sdkTestContext.ts +++ b/nodejs/test/e2e/harness/sdkTestContext.ts @@ -16,6 +16,29 @@ import { formatError, retry } from "./sdkTestHelper"; export const isCI = process.env.GITHUB_ACTIONS === "true"; export const DEFAULT_GITHUB_TOKEN = "fake-token-for-e2e-tests"; +/** + * True when the E2E suite is running over the in-process (FFI) transport + * (COPILOT_SDK_DEFAULT_CONNECTION=inprocess). Use with `it.skipIf` / `describe.skipIf` + * to skip tests for features that are not supported over the in-process transport (the + * runtime loads into the shared host process), so the in-process CI cell stays green. + * Such features are covered by the default (stdio) cell. + */ +export const isInProcessTransport = + (process.env.COPILOT_SDK_DEFAULT_CONNECTION ?? "").toLowerCase() === "inprocess"; + +// The in-process (FFI) transport resolves auth host-side, in this test process, and +// ranks HMAC above the GitHub token — so an ambient COPILOT_HMAC_KEY (CI sets one as a +// job-level credential) would be picked over the SDK/Bearer token the replay snapshots +// expect, yielding 401s. Host-side auth can capture the key as early as client +// construction (before any per-test beforeEach runs), so neutralize it at module load — +// the analogue of .NET's InProcessEnvIsolation `[ModuleInitializer]`. Only applied for +// the in-process transport; stdio/tcp children resolve auth in their own process where +// the token already outranks HMAC. See https://github.com/github/copilot-sdk/issues/1934. +if ((process.env.COPILOT_SDK_DEFAULT_CONNECTION ?? "").toLowerCase() === "inprocess") { + delete process.env.COPILOT_HMAC_KEY; + delete process.env.CAPI_HMAC_KEY; +} + const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const SNAPSHOTS_DIR = resolve(__dirname, "../../../../test/snapshots"); @@ -69,8 +92,14 @@ export async function createSdkTestContext({ COPILOT_HOME: copilotHomeDir, COPILOT_SDK_AUTH_TOKEN: "", GH_CONFIG_DIR: homeDir, - GH_TOKEN: "", - GITHUB_TOKEN: "", + // Use the proxy-recognized token rather than blanking these. Tests that spin up + // their own client without passing `gitHubToken` (e.g. the stdio/tcp + // "works without onPermissionRequest" cases) rely on GH_TOKEN/GITHUB_TOKEN to + // authenticate against the replay proxy. Blanking them only worked on CI, where an + // ambient COPILOT_HMAC_KEY secret supplies the credential instead; locally there is + // no HMAC key, so the child CLI had nothing to authenticate with and got a 401. + GH_TOKEN: authTokenToUse, + GITHUB_TOKEN: authTokenToUse, // TODO: I'm not convinced the SDK should default to using whatever config you happen to have in your homedir. // The SDK config should be independent of the regular CLI app. Likewise it shouldn't mix sessions from the @@ -102,11 +131,17 @@ export async function createSdkTestContext({ } else { connection = userConn; } + } else if (useStdio === false) { + connection = RuntimeConnection.forTcp({ path: cliPath }); + } else if ( + useStdio === undefined && + (process.env.COPILOT_SDK_DEFAULT_CONNECTION ?? "").toLowerCase() === "inprocess" + ) { + // The in-process FFI transport resolves the CLI entrypoint itself + // (COPILOT_CLI_PATH or the bundled platform package), so no path is passed. + connection = RuntimeConnection.forInProcess(); } else { - connection = - useStdio === false - ? RuntimeConnection.forTcp({ path: cliPath }) - : RuntimeConnection.forStdio({ path: cliPath }); + connection = RuntimeConnection.forStdio({ path: cliPath }); } const { @@ -114,20 +149,92 @@ export async function createSdkTestContext({ env: userEnv, ...remainingClientOptions } = copilotClientOptions ?? {}; - const copilotClient = new CopilotClient({ - workingDirectory: workDir, - env: { ...env, ...userEnv }, - logLevel: logLevel || "error", - connection, - gitHubToken: authTokenToUse, - ...remainingClientOptions, - }); - const harness = { homeDir, workDir, openAiEndpoint, copilotClient, env }; + const mergedEnv = { ...env, ...userEnv }; + + // The in-process (FFI) transport loads the runtime into this test host process, + // and its worker inherits this process's ambient environment rather than a + // per-client env block (see https://github.com/github/copilot-sdk/issues/1934). + // So the per-test redirects, isolated home, and credentials must be mirrored onto + // the real process environment. Node's `process.env` writes reach native `getenv`, + // so host-side runtime reads (auth resolution, GitHub API redirect) observe them. + // Auth flows via GH_TOKEN/GITHUB_TOKEN here (the FFI argv omits the stdio + // `--auth-token-env COPILOT_SDK_AUTH_TOKEN` wiring), and HMAC is disabled so + // host-side auth resolution picks the SDK/Bearer token the replay snapshots expect. + const isInProcess = connection.kind === "inprocess"; + const inProcessEnv: Record = isInProcess + ? { + ...(mergedEnv as Record), + GH_TOKEN: authTokenToUse, + GITHUB_TOKEN: authTokenToUse, + COPILOT_HMAC_KEY: "", + CAPI_HMAC_KEY: "", + } + : {}; + + // Builds a CopilotClient wired for the active transport, so tests that need a + // secondary client (e.g. resuming a session from a fresh client) don't have to + // reimplement the in-process env/cwd handling. Callers may override the connection + // (e.g. pin stdio for telemetry, which the in-process transport cannot carry + // per-client); env is attached to child-process transports and mirrored onto the + // process for in-process (see beforeEach below), never passed per-client for the + // in-process transport where it would be rejected. + function createClient(overrides: Partial = {}): CopilotClient { + const { + connection: overrideConnection, + env: _ignoredEnv, + workingDirectory: overrideWorkingDirectory, + ...rest + } = overrides; + + let effectiveConnection = overrideConnection ?? connection; + // Fill in the bundled CLI path for child-process connections that omit it + // (e.g. a bare RuntimeConnection.forStdio() used to pin telemetry to stdio). + if (effectiveConnection.kind === "stdio" && effectiveConnection.path === undefined) { + effectiveConnection = RuntimeConnection.forStdio({ + ...effectiveConnection, + path: cliPath, + }); + } else if (effectiveConnection.kind === "tcp" && effectiveConnection.path === undefined) { + effectiveConnection = RuntimeConnection.forTcp({ + ...effectiveConnection, + path: cliPath, + }); + } + const effectiveInProcess = effectiveConnection.kind === "inprocess"; + + return new CopilotClient({ + // The in-process transport rejects a per-client workingDirectory (it would have to + // mutate the shared host process cwd). Instead the harness changes this process's + // cwd to workDir around the in-process worker's startup (see beforeEach below), so + // the worker still spawns with workDir as its cwd. Out-of-process clients get it + // as a normal per-client option. + workingDirectory: + overrideWorkingDirectory ?? (effectiveInProcess ? undefined : workDir), + // In-process hosting mirrors the environment onto the real process (per test, in + // beforeEach below), so the worker inherits it; passing a per-client env here + // would have no effect (and is rejected by the in-process transport). + env: effectiveInProcess ? undefined : mergedEnv, + logLevel: logLevel || "error", + connection: effectiveConnection, + gitHubToken: authTokenToUse, + ...rest, + }); + } + + const copilotClient = createClient(remainingClientOptions); + + const harness = { homeDir, workDir, openAiEndpoint, copilotClient, env, createClient }; // Track if any test fails to avoid writing corrupted snapshots let anyTestFailed = false; + // Holds the process.env entries the current test overwrote, so afterEach restores them. + let restoreProcessEnv: Array<[string, string | undefined]> = []; + + // Holds the process cwd before an in-process test changed it, so afterEach restores it. + let restoreCwd: string | undefined; + // Wire up to Vitest lifecycle beforeEach(async (testContext) => { // Must be inside beforeEach - vitest requires test context @@ -135,6 +242,25 @@ export async function createSdkTestContext({ anyTestFailed = true; }); + // Mirror this context's environment onto the real process for in-process + // hosting, right before the test runs (see the comment above the client). The + // client auto-starts on first use inside the test body, so the worker spawns + // under these values. + restoreProcessEnv = []; + for (const [key, value] of Object.entries(inProcessEnv)) { + restoreProcessEnv.push([key, process.env[key]]); + process.env[key] = value; + } + + // The in-process worker inherits this process's cwd at spawn (the client auto-starts + // on first use inside the test body). Point cwd at workDir here so the worker spawns + // with the same working directory the out-of-process transport passes explicitly; + // afterEach restores it. + if (isInProcess) { + restoreCwd = process.cwd(); + process.chdir(workDir); + } + await openAiEndpoint.updateConfig({ filePath: getTrafficCapturePath(testContext), workDir, @@ -146,6 +272,20 @@ export async function createSdkTestContext({ }); afterEach(async () => { + // Undo this test's process.env mirror so it can't leak into the next test/suite. + for (const [key, previous] of restoreProcessEnv.reverse()) { + if (previous === undefined) { + delete process.env[key]; + } else { + process.env[key] = previous; + } + } + restoreProcessEnv = []; + // Restore the cwd an in-process test changed for worker startup. + if (restoreCwd !== undefined) { + process.chdir(restoreCwd); + restoreCwd = undefined; + } // Empty directories but leave them in place for next test await rimraf([join(homeDir, "*"), join(workDir, "*")], { glob: true }); }); @@ -153,7 +293,15 @@ export async function createSdkTestContext({ afterAll(async () => { await copilotClient.stop(); await openAiEndpoint.stop(anyTestFailed); - await rmDir("remove e2e test copilotHomeDir", copilotHomeDir); + // On Windows, this Vitest worker can retain the in-process runtime's session.db + // lock until the worker exits. Retrying from its afterAll hook cannot succeed: + // the hook waits for the lock, while the lock cannot clear until the hook returns + // and lets the worker exit. + await rmDir( + "remove e2e test copilotHomeDir", + copilotHomeDir, + isInProcess && process.platform === "win32" ? 1 : 30 + ); await rmDir("remove e2e test homeDir", homeDir); await rmDir("remove e2e test workDir", workDir); }); @@ -180,14 +328,14 @@ function getTrafficCapturePath(testContext: TestContext): string { return join(SNAPSHOTS_DIR, testFileName, `${taskNameAsFilename}.yaml`); } -async function rmDir(message: string, path: string): Promise { +async function rmDir(message: string, path: string, maxTries = 30): Promise { // Use longer retries to tolerate Windows holding SQLite session-store.db // open briefly after the CLI subprocess exits. If the temp dir still can't // be removed (e.g. CLI background writer racing with cleanup), warn and // continue rather than failing the whole test run — the OS / CI runner // will reclaim the temp dir on shutdown. try { - await retry(message, () => rm(path, { recursive: true, force: true }), 30, 1000); + await retry(message, () => rm(path, { recursive: true, force: true }), maxTries, 1000); } catch (error) { console.warn( `WARN: ${message} failed; leaving temp dir for OS cleanup: ${formatError(error)}` diff --git a/nodejs/test/e2e/inprocess_ffi.e2e.test.ts b/nodejs/test/e2e/inprocess_ffi.e2e.test.ts new file mode 100644 index 0000000000..af879ea77b --- /dev/null +++ b/nodejs/test/e2e/inprocess_ffi.e2e.test.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import { CopilotClient, RuntimeConnection } from "../../src/index.js"; + +describe("In-process FFI transport", () => { + // Smoke test that the in-process FFI transport starts and completes a round-trip. + // Resolution of the in-process transport from COPILOT_SDK_DEFAULT_CONNECTION is + // exercised by the full E2E suite running under the `inprocess` CI matrix cell, + // not a dedicated test. + it("should start and connect over in-process FFI", async () => { + // In-process FFI hosting resolves the CLI entrypoint (COPILOT_CLI_PATH or the + // bundled platform package) and its sibling native runtime library itself. If + // neither is available, start() throws and the test fails hard. + const client = new CopilotClient({ connection: RuntimeConnection.forInProcess() }); + await client.start(); + + const pong = await client.ping("ffi message"); + expect(pong.message).toBe("pong: ffi message"); + expect(Date.parse(pong.timestamp)).not.toBeNaN(); + + expect(await client.stop()).toHaveLength(0); // No errors on stop + }); +}); diff --git a/nodejs/test/e2e/mode_handlers.e2e.test.ts b/nodejs/test/e2e/mode_handlers.e2e.test.ts index 8e2b8aed67..71c4b08963 100644 --- a/nodejs/test/e2e/mode_handlers.e2e.test.ts +++ b/nodejs/test/e2e/mode_handlers.e2e.test.ts @@ -110,7 +110,7 @@ describe("Mode handlers", async () => { expect(exitPlanModeRequests).toHaveLength(1); expect(exitPlanModeRequests[0]).toMatchObject({ summary: PLAN_SUMMARY, - actions: ["interactive", "autopilot", "exit_only"], + actions: ["autopilot", "interactive", "exit_only"], recommendedAction: "interactive", }); expect(exitPlanModeRequests[0].planContent).toBeDefined(); diff --git a/nodejs/test/e2e/multi-client.e2e.test.ts b/nodejs/test/e2e/multi-client.e2e.test.ts index a63b1b0ebf..a44ceec3c3 100644 --- a/nodejs/test/e2e/multi-client.e2e.test.ts +++ b/nodejs/test/e2e/multi-client.e2e.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it, afterAll } from "vitest"; import { z } from "zod"; import { CopilotClient, defineTool, approveAll, RuntimeConnection } from "../../src/index.js"; import type { SessionEvent } from "../../src/index.js"; -import { createSdkTestContext } from "./harness/sdkTestContext"; +import { createSdkTestContext, isInProcessTransport } from "./harness/sdkTestContext"; describe("Multi-client broadcast", async () => { // Use TCP mode so a second client can connect to the same CLI process @@ -304,71 +304,75 @@ describe("Multi-client broadcast", async () => { } ); - it("disconnecting client removes its tools", { timeout: 90_000 }, async () => { - const toolA = defineTool("stable_tool", { - description: "A tool that persists across disconnects", - parameters: z.object({ input: z.string() }), - handler: ({ input }) => `STABLE_${input}`, - }); + it.skipIf(isInProcessTransport)( + "disconnecting client removes its tools", + { timeout: 90_000 }, + async () => { + const toolA = defineTool("stable_tool", { + description: "A tool that persists across disconnects", + parameters: z.object({ input: z.string() }), + handler: ({ input }) => `STABLE_${input}`, + }); - const toolB = defineTool("ephemeral_tool", { - description: "A tool that will disappear when its client disconnects", - parameters: z.object({ input: z.string() }), - handler: ({ input }) => `EPHEMERAL_${input}`, - }); + const toolB = defineTool("ephemeral_tool", { + description: "A tool that will disappear when its client disconnects", + parameters: z.object({ input: z.string() }), + handler: ({ input }) => `EPHEMERAL_${input}`, + }); - // Client 1 creates a session with stable_tool - const session1 = await client1.createSession({ - onPermissionRequest: approveAll, - tools: [toolA], - }); + // Client 1 creates a session with stable_tool + const session1 = await client1.createSession({ + onPermissionRequest: approveAll, + tools: [toolA], + }); - // Client 2 resumes with ephemeral_tool - await client2.resumeSession(session1.sessionId, { - onPermissionRequest: approveAll, - tools: [toolB], - }); + // Client 2 resumes with ephemeral_tool + await client2.resumeSession(session1.sessionId, { + onPermissionRequest: approveAll, + tools: [toolB], + }); - // Verify both tools work before disconnect (sequential to avoid nondeterministic tool_call ordering) - const stableResponse = await session1.sendAndWait({ - prompt: "Use the stable_tool with input 'test1' and tell me the result.", - }); - expect(stableResponse?.data.content).toContain("STABLE_test1"); + // Verify both tools work before disconnect (sequential to avoid nondeterministic tool_call ordering) + const stableResponse = await session1.sendAndWait({ + prompt: "Use the stable_tool with input 'test1' and tell me the result.", + }); + expect(stableResponse?.data.content).toContain("STABLE_test1"); - const ephemeralResponse = await session1.sendAndWait({ - prompt: "Use the ephemeral_tool with input 'test2' and tell me the result.", - }); - expect(ephemeralResponse?.data.content).toContain("EPHEMERAL_test2"); - - // Disconnect client 2 without destroying the shared session. - // Suppress "Connection is disposed" rejections that occur when the server - // broadcasts events (e.g. tool_changed_notice) to the now-dead connection. - const suppressDisposed = (reason: unknown) => { - if (reason instanceof Error && reason.message.includes("Connection is disposed")) { - return; - } - throw reason; - }; - process.on("unhandledRejection", suppressDisposed); - await client2.forceStop(); - - // Give the server time to process the connection close and remove tools - await new Promise((resolve) => setTimeout(resolve, 500)); - process.removeListener("unhandledRejection", suppressDisposed); - - // Recreate client2 for cleanup in afterAll (but don't rejoin the session) - client2 = new CopilotClient({ - connection: RuntimeConnection.forUri(`localhost:${runtimePort}`, { - connectionToken: tcpConnectionToken, - }), - }); + const ephemeralResponse = await session1.sendAndWait({ + prompt: "Use the ephemeral_tool with input 'test2' and tell me the result.", + }); + expect(ephemeralResponse?.data.content).toContain("EPHEMERAL_test2"); + + // Disconnect client 2 without destroying the shared session. + // Suppress "Connection is disposed" rejections that occur when the server + // broadcasts events (e.g. tool_changed_notice) to the now-dead connection. + const suppressDisposed = (reason: unknown) => { + if (reason instanceof Error && reason.message.includes("Connection is disposed")) { + return; + } + throw reason; + }; + process.on("unhandledRejection", suppressDisposed); + await client2.forceStop(); + + // Give the server time to process the connection close and remove tools + await new Promise((resolve) => setTimeout(resolve, 500)); + process.removeListener("unhandledRejection", suppressDisposed); + + // Recreate client2 for cleanup in afterAll (but don't rejoin the session) + client2 = new CopilotClient({ + connection: RuntimeConnection.forUri(`localhost:${runtimePort}`, { + connectionToken: tcpConnectionToken, + }), + }); - // Now only stable_tool should be available - const afterResponse = await session1.sendAndWait({ - prompt: "Use the stable_tool with input 'still_here'. Also try using ephemeral_tool if it is available.", - }); - expect(afterResponse?.data.content).toContain("STABLE_still_here"); - // ephemeral_tool should NOT have produced a result - expect(afterResponse?.data.content).not.toContain("EPHEMERAL_"); - }); + // Now only stable_tool should be available + const afterResponse = await session1.sendAndWait({ + prompt: "Use the stable_tool with input 'still_here'. Also try using ephemeral_tool if it is available.", + }); + expect(afterResponse?.data.content).toContain("STABLE_still_here"); + // ephemeral_tool should NOT have produced a result + expect(afterResponse?.data.content).not.toContain("EPHEMERAL_"); + } + ); }); diff --git a/nodejs/test/e2e/pending_work_resume.e2e.test.ts b/nodejs/test/e2e/pending_work_resume.e2e.test.ts index 60bb2399e8..85abc3a900 100644 --- a/nodejs/test/e2e/pending_work_resume.e2e.test.ts +++ b/nodejs/test/e2e/pending_work_resume.e2e.test.ts @@ -57,6 +57,20 @@ async function waitWithTimeout( } } +async function waitForPendingPermissionRequestId(session: CopilotSession): Promise { + const deadline = Date.now() + PENDING_WORK_TIMEOUT_MS; + do { + const pending = await session.rpc.permissions.pendingRequests(); + const request = pending.items[0]; + if (request) { + return request.requestId; + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } while (Date.now() < deadline); + + throw new Error("Timeout waiting for pending permission request"); +} + function waitForExternalToolRequests( session: CopilotSession, toolNames: string[] @@ -205,7 +219,7 @@ describe("Pending work resume", async () => { PENDING_WORK_TIMEOUT_MS, "originalPermissionRequest" ); - const permissionEvent = await permissionRequestedP; + await permissionRequestedP; expect(initialRequest.kind).toBe("custom-tool"); await suspendedClient.forceStop(); @@ -222,10 +236,11 @@ describe("Pending work resume", async () => { }), ], }); + const requestId = await waitForPendingPermissionRequestId(session2); const permissionResult = await session2.rpc.permissions.handlePendingPermissionRequest({ - requestId: permissionEvent.data.requestId, + requestId, result: { kind: "approve-once" }, }); expect(permissionResult.success).toBe(true); diff --git a/nodejs/test/e2e/rpc.e2e.test.ts b/nodejs/test/e2e/rpc.e2e.test.ts index 0442ab9267..f90547da9b 100644 --- a/nodejs/test/e2e/rpc.e2e.test.ts +++ b/nodejs/test/e2e/rpc.e2e.test.ts @@ -2,10 +2,10 @@ import { describe, expect, it, onTestFinished } from "vitest"; import { CopilotClient, approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; -function onTestFinishedForceStop(client: CopilotClient) { +function onTestFinishedStop(client: CopilotClient) { onTestFinished(async () => { try { - await client.forceStop(); + await client.stop(); } catch { // Ignore cleanup errors - process may already be stopped } @@ -15,7 +15,7 @@ function onTestFinishedForceStop(client: CopilotClient) { describe("RPC", () => { it("should call rpc.ping with typed params and result", async () => { const client = new CopilotClient(); - onTestFinishedForceStop(client); + onTestFinishedStop(client); await client.start(); @@ -28,7 +28,7 @@ describe("RPC", () => { it("should call rpc.models.list with typed result", async () => { const client = new CopilotClient(); - onTestFinishedForceStop(client); + onTestFinishedStop(client); await client.start(); @@ -48,7 +48,7 @@ describe("RPC", () => { // account.getQuota is defined in schema but not yet implemented in CLI it.skip("should call rpc.account.getQuota when authenticated", async () => { const client = new CopilotClient(); - onTestFinishedForceStop(client); + onTestFinishedStop(client); await client.start(); diff --git a/nodejs/test/e2e/rpc_mcp_and_skills.e2e.test.ts b/nodejs/test/e2e/rpc_mcp_and_skills.e2e.test.ts index cdd64017c1..4025dc444c 100644 --- a/nodejs/test/e2e/rpc_mcp_and_skills.e2e.test.ts +++ b/nodejs/test/e2e/rpc_mcp_and_skills.e2e.test.ts @@ -74,7 +74,7 @@ describe("Session MCP and skills RPC", async () => { }); onTestFinished(async () => { try { - await mcpAppsClient.forceStop(); + await mcpAppsClient.stop(); } catch { // Ignore cleanup errors } diff --git a/nodejs/test/e2e/rpc_mcp_config.e2e.test.ts b/nodejs/test/e2e/rpc_mcp_config.e2e.test.ts index 581567cb3e..95694a8c63 100644 --- a/nodejs/test/e2e/rpc_mcp_config.e2e.test.ts +++ b/nodejs/test/e2e/rpc_mcp_config.e2e.test.ts @@ -9,7 +9,7 @@ function startEphemeralClient(): CopilotClient { const client = new CopilotClient(); onTestFinished(async () => { try { - await client.forceStop(); + await client.stop(); } catch { // Ignore cleanup errors } diff --git a/nodejs/test/e2e/rpc_server.e2e.test.ts b/nodejs/test/e2e/rpc_server.e2e.test.ts index 8913146b12..5075ae68d9 100644 --- a/nodejs/test/e2e/rpc_server.e2e.test.ts +++ b/nodejs/test/e2e/rpc_server.e2e.test.ts @@ -39,7 +39,7 @@ describe("Server-scoped RPC", async () => { }); onTestFinished(async () => { try { - await extraClient.forceStop(); + await extraClient.stop(); } catch { // Ignore cleanup errors } diff --git a/nodejs/test/e2e/rpc_server_misc.e2e.test.ts b/nodejs/test/e2e/rpc_server_misc.e2e.test.ts index c38fccca17..4f12e507a5 100644 --- a/nodejs/test/e2e/rpc_server_misc.e2e.test.ts +++ b/nodejs/test/e2e/rpc_server_misc.e2e.test.ts @@ -65,7 +65,7 @@ describe("Miscellaneous server-scoped RPC", async () => { async function disposeIsolated(isolatedClient: CopilotClient, home: string): Promise { try { - await isolatedClient.forceStop(); + await isolatedClient.stop(); } catch { // Best-effort cleanup. } @@ -74,7 +74,7 @@ describe("Miscellaneous server-scoped RPC", async () => { async function forceStop(target: CopilotClient): Promise { try { - await target.forceStop(); + await target.stop(); } catch { // Runtime may already be gone. } diff --git a/nodejs/test/e2e/rpc_server_plugins.e2e.test.ts b/nodejs/test/e2e/rpc_server_plugins.e2e.test.ts index 4bc3c63c96..20575a9114 100644 --- a/nodejs/test/e2e/rpc_server_plugins.e2e.test.ts +++ b/nodejs/test/e2e/rpc_server_plugins.e2e.test.ts @@ -59,7 +59,7 @@ describe("Server-scoped plugin RPC", async () => { fixtureDir?: string ): Promise { try { - await client.forceStop(); + await client.stop(); } catch { // Best-effort cleanup. } diff --git a/nodejs/test/e2e/rpc_server_remote_control.e2e.test.ts b/nodejs/test/e2e/rpc_server_remote_control.e2e.test.ts index 2e6c6cf05b..3094d32577 100644 --- a/nodejs/test/e2e/rpc_server_remote_control.e2e.test.ts +++ b/nodejs/test/e2e/rpc_server_remote_control.e2e.test.ts @@ -23,7 +23,7 @@ describe("Server-scoped remote-control RPC", async () => { async function forceStop(client: CopilotClient): Promise { try { - await client.forceStop(); + await client.stop(); } catch { // Runtime may already be gone. } 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 6e84a6f669..54f40fe12c 100644 --- a/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts +++ b/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts @@ -84,7 +84,7 @@ describe("Session-scoped state extras RPC", async () => { } finally { await disconnect(session); try { - await authClient.forceStop(); + await authClient.stop(); } catch { // Best-effort cleanup. } diff --git a/nodejs/test/e2e/rpc_workspace_checkpoints.e2e.test.ts b/nodejs/test/e2e/rpc_workspace_checkpoints.e2e.test.ts index d7f478e1f1..78a820f67a 100644 --- a/nodejs/test/e2e/rpc_workspace_checkpoints.e2e.test.ts +++ b/nodejs/test/e2e/rpc_workspace_checkpoints.e2e.test.ts @@ -23,9 +23,9 @@ describe("Session workspace checkpoint RPC", async () => { it("should return null or empty content for unknown checkpoint", async () => { const session = await client.createSession({ onPermissionRequest: approveAll }); try { - const result = await session.rpc.workspaces.readCheckpoint({ - number: Number.MAX_SAFE_INTEGER, - }); + // A high but 32-bit-safe checkpoint number that will never exist in a fresh + // session, so the read reports the checkpoint as missing. + const result = await session.rpc.workspaces.readCheckpoint({ number: 4294967294 }); expect(result.content ?? "").toBe(""); } finally { await session.disconnect(); diff --git a/nodejs/test/e2e/session.e2e.test.ts b/nodejs/test/e2e/session.e2e.test.ts index c29f0790d0..2cb88917e3 100644 --- a/nodejs/test/e2e/session.e2e.test.ts +++ b/nodejs/test/e2e/session.e2e.test.ts @@ -5,15 +5,16 @@ import { CopilotClient, approveAll, defineTool, RuntimeConnection } from "../../ import { createSdkTestContext, DEFAULT_GITHUB_TOKEN, isCI } from "./harness/sdkTestContext.js"; import { getFinalAssistantMessage, getNextEventOfType, retry } from "./harness/sdkTestHelper.js"; -describe("Sessions", async () => { - const { - copilotClient: client, - openAiEndpoint, - homeDir, - workDir, - env, - } = await createSdkTestContext(); - +const { + copilotClient: client, + openAiEndpoint, + homeDir, + workDir, + env, + createClient, +} = await createSdkTestContext(); + +describe("Sessions", () => { async function waitForExchanges(minimumCount = 1) { await retry( `capture ${minimumCount} chat completion request(s)`, @@ -39,15 +40,14 @@ describe("Sessions", async () => { }); onTestFinished(async () => { try { - await standaloneClient.forceStop(); + await standaloneClient.stop(); } catch { // ignore } }); - const session = await standaloneClient.createSession({}); + await using session = await standaloneClient.createSession({}); expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); - await session.disconnect(); } ); @@ -64,7 +64,7 @@ describe("Sessions", async () => { }); onTestFinished(async () => { try { - await tcpClient.forceStop(); + await tcpClient.stop(); } catch { // ignore } @@ -84,7 +84,7 @@ describe("Sessions", async () => { }); onTestFinished(async () => { try { - await resumeClient.forceStop(); + await resumeClient.stop(); } catch { // ignore } @@ -96,7 +96,7 @@ describe("Sessions", async () => { await originalSession.disconnect(); }); it("should create and disconnect sessions", async () => { - const session = await client.createSession({ + await using session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-sonnet-4.5", }); @@ -118,7 +118,7 @@ describe("Sessions", async () => { // TODO: Re-enable once test harness CAPI proxy supports this test's session lifecycle it.skip("should list sessions with context field", { timeout: 60000 }, async () => { // Create a session — just creating it is enough for it to appear in listSessions - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); // Verify it has a start event (confirms session is active) @@ -137,7 +137,7 @@ describe("Sessions", async () => { }); it("should get session metadata by ID", { timeout: 60000 }, async () => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); // Send a message to persist the session to disk @@ -164,7 +164,7 @@ describe("Sessions", async () => { }); it("should have stateful conversation", async () => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); const assistantMessage = await session.sendAndWait({ prompt: "What is 1+1?" }); expect(assistantMessage?.data.content).toContain("2"); @@ -176,7 +176,7 @@ describe("Sessions", async () => { it("should create a session with appended systemMessage config", async () => { const systemMessageSuffix = "End each response with the phrase 'Have a nice day!'"; - const session = await client.createSession({ + await using session = await client.createSession({ onPermissionRequest: approveAll, systemMessage: { mode: "append", @@ -197,7 +197,7 @@ describe("Sessions", async () => { it("should create a session with replaced systemMessage config", async () => { const testSystemMessage = "You are an assistant called Testy McTestface. Reply succinctly."; - const session = await client.createSession({ + await using session = await client.createSession({ onPermissionRequest: approveAll, systemMessage: { mode: "replace", content: testSystemMessage }, }); @@ -218,7 +218,7 @@ describe("Sessions", async () => { async () => { const customTone = "Respond in a warm, professional tone. Be thorough in explanations."; const appendedContent = "Always mention quarterly earnings."; - const session = await client.createSession({ + await using session = await client.createSession({ onPermissionRequest: approveAll, systemMessage: { mode: "customize", @@ -230,66 +230,54 @@ describe("Sessions", async () => { }, }); - try { - await session.send({ prompt: "Who are you?" }); - - // Validate the system message sent to the model - const traffic = await waitForExchanges(); - const systemMessage = getSystemMessage(traffic[0]); - expect(systemMessage).toContain(customTone); - expect(systemMessage).toContain(appendedContent); - // The code_change_rules section should have been removed - expect(systemMessage).not.toContain(""); - } finally { - await session.disconnect(); - } + await session.send({ prompt: "Who are you?" }); + + // Validate the system message sent to the model + const traffic = await waitForExchanges(); + const systemMessage = getSystemMessage(traffic[0]); + expect(systemMessage).toContain(customTone); + expect(systemMessage).toContain(appendedContent); + // The code_change_rules section should have been removed + expect(systemMessage).not.toContain(""); } ); it("should create a session with availableTools", async () => { - const session = await client.createSession({ + await using session = await client.createSession({ onPermissionRequest: approveAll, availableTools: ["view", "edit"], }); - try { - await session.send({ prompt: "What is 1+1?" }); + await session.send({ prompt: "What is 1+1?" }); - // It only tells the model about the specified tools and no others - const traffic = await waitForExchanges(); - expect(traffic[0].request.tools).toMatchObject([ - { function: { name: "view" } }, - { function: { name: "edit" } }, - ]); - } finally { - await session.disconnect(); - } + // It only tells the model about the specified tools and no others + const traffic = await waitForExchanges(); + expect(traffic[0].request.tools).toMatchObject([ + { function: { name: "view" } }, + { function: { name: "edit" } }, + ]); }); it("should create a session with excludedTools", async () => { - const session = await client.createSession({ + await using session = await client.createSession({ onPermissionRequest: approveAll, excludedTools: ["view"], }); - try { - await session.send({ prompt: "What is 1+1?" }); + await session.send({ prompt: "What is 1+1?" }); - // It has other tools, but not the one we excluded - const traffic = await waitForExchanges(); - const functionNames = traffic[0].request.tools?.map( - (t) => (t as { function: { name: string } }).function.name - ); - expect(functionNames).toContain("edit"); - expect(functionNames).toContain("grep"); - expect(functionNames).not.toContain("view"); - } finally { - await session.disconnect(); - } + // It has other tools, but not the one we excluded + const traffic = await waitForExchanges(); + const functionNames = traffic[0].request.tools?.map( + (t) => (t as { function: { name: string } }).function.name + ); + expect(functionNames).toContain("edit"); + expect(functionNames).toContain("grep"); + expect(functionNames).not.toContain("view"); }); it("should create a session with defaultAgent excludedTools", async () => { - const session = await client.createSession({ + await using session = await client.createSession({ onPermissionRequest: approveAll, tools: [ defineTool("secret_tool", { @@ -307,19 +295,15 @@ describe("Sessions", async () => { }, }); - try { - await session.send({ prompt: "What is 1+1?" }); + await session.send({ prompt: "What is 1+1?" }); - // The secret_tool should be registered with the runtime but not advertised - // to the default agent's underlying model call. - const traffic = await waitForExchanges(); - const functionNames = traffic[0].request.tools?.map( - (t) => (t as { function: { name: string } }).function.name - ); - expect(functionNames).not.toContain("secret_tool"); - } finally { - await session.disconnect(); - } + // The secret_tool should be registered with the runtime but not advertised + // to the default agent's underlying model call. + const traffic = await waitForExchanges(); + const functionNames = traffic[0].request.tools?.map( + (t) => (t as { function: { name: string } }).function.name + ); + expect(functionNames).not.toContain("secret_tool"); }); // TODO: This test shows there's a race condition inside client.ts. If createSession is called @@ -362,7 +346,9 @@ describe("Sessions", async () => { expect(answer?.data.content).toContain("2"); // Resume using the same client - const session2 = await client.resumeSession(sessionId, { onPermissionRequest: approveAll }); + await using session2 = await client.resumeSession(sessionId, { + onPermissionRequest: approveAll, + }); expect(session2.sessionId).toBe(sessionId); const messages = await session2.getEvents(); const assistantMessages = messages.filter((m) => m.type === "assistant.message"); @@ -377,19 +363,18 @@ describe("Sessions", async () => { it("should resume a session using a new client", async () => { // Create initial session - const session1 = await client.createSession({ onPermissionRequest: approveAll }); + await using session1 = await client.createSession({ onPermissionRequest: approveAll }); const sessionId = session1.sessionId; const answer = await session1.sendAndWait({ prompt: "What is 1+1?" }); expect(answer?.data.content).toContain("2"); // Resume using a new client - const newClient = new CopilotClient({ - env, + const newClient = createClient({ gitHubToken: isCI ? "fake-token-for-e2e-tests" : undefined, }); - onTestFinished(() => newClient.forceStop()); - const session2 = await newClient.resumeSession(sessionId, { + onTestFinished(() => newClient.stop()); + await using session2 = await newClient.resumeSession(sessionId, { onPermissionRequest: approveAll, }); expect(session2.sessionId).toBe(sessionId); @@ -417,7 +402,7 @@ describe("Sessions", async () => { }); it("should create session with custom tool", async () => { - const session = await client.createSession({ + await using session = await client.createSession({ onPermissionRequest: approveAll, tools: [ { @@ -452,7 +437,7 @@ describe("Sessions", async () => { const sessionId = session.sessionId; // Resume the session with a provider - const session2 = await client.resumeSession(sessionId, { + await using session2 = await client.resumeSession(sessionId, { onPermissionRequest: approveAll, provider: { type: "openai", @@ -467,7 +452,7 @@ describe("Sessions", async () => { it("resumes a persisted session from a new client when an MCP OAuth handler is configured", async () => { // Take a turn so the session is persisted to the store and can be // loaded by a different CLI process. - const session1 = await client.createSession({ + await using session1 = await client.createSession({ onPermissionRequest: approveAll, onMcpAuthRequest: () => ({ kind: "cancelled" }), }); @@ -481,25 +466,23 @@ describe("Sessions", async () => { // `session.eventLog.registerInterest` for `mcp.oauth_required`; that must // be sent AFTER `session.resume`, otherwise the runtime rejects it with // "Session not found: ". - const newClient = new CopilotClient({ - env, + const newClient = createClient({ gitHubToken: isCI ? DEFAULT_GITHUB_TOKEN : (process.env.GITHUB_TOKEN ?? DEFAULT_GITHUB_TOKEN), }); - onTestFinished(() => newClient.forceStop()); + onTestFinished(() => newClient.stop()); - const session2 = await newClient.resumeSession(sessionId, { + await using session2 = await newClient.resumeSession(sessionId, { onPermissionRequest: approveAll, onMcpAuthRequest: () => ({ kind: "cancelled" }), }); expect(session2.sessionId).toBe(sessionId); - await session2.disconnect(); }); it("should abort a session", async () => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); // Set up event listeners BEFORE sending to avoid race conditions const nextToolCallStart = getNextEventOfType(session, "tool.execution_start"); @@ -530,7 +513,7 @@ describe("Sessions", async () => { // if the session weren't registered in the sessions map before the RPC, // the event would be dropped. const earlyEvents: Array<{ type: string }> = []; - const session = await client.createSession({ + await using session = await client.createSession({ onPermissionRequest: approveAll, onEvent: (event) => { earlyEvents.push(event); @@ -562,7 +545,7 @@ describe("Sessions", async () => { }); it("handler exception does not halt event delivery", async () => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); let eventCount = 0; let gotIdle = false; @@ -587,12 +570,10 @@ describe("Sessions", async () => { // Handler saw more than just the first (throwing) event. expect(eventCount).toBeGreaterThan(1); - - await session.disconnect(); }); it("disposeAsync from handler does not deadlock", async () => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); let disposed = false; const disposedPromise = new Promise((resolve) => { @@ -619,25 +600,21 @@ describe("Sessions", async () => { onTestFinished(async () => { await rm(customConfigDir, { recursive: true, force: true }).catch(() => {}); }); - const session = await client.createSession({ + await using session = await client.createSession({ onPermissionRequest: approveAll, configDirectory: customConfigDir, }); expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); - try { - // Session should work normally with custom config dir - await session.send({ prompt: "What is 1+1?" }); - const assistantMessage = await getFinalAssistantMessage(session); - expect(assistantMessage.data.content).toContain("2"); - } finally { - await session.disconnect(); - } + // Session should work normally with custom config dir + await session.send({ prompt: "What is 1+1?" }); + const assistantMessage = await getFinalAssistantMessage(session); + expect(assistantMessage.data.content).toContain("2"); }); it("should log messages at all levels and emit matching session events", async () => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); const events: Array<{ type: string; id?: string; data?: Record }> = []; session.on((event) => { @@ -692,7 +669,7 @@ describe("Sessions", async () => { const { writeFile } = await import("fs/promises"); await writeFile(filePath, "FILE_ATTACHMENT_SENTINEL"); - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); await session.sendAndWait({ prompt: "Read the attached file and reply with its contents.", @@ -726,8 +703,6 @@ describe("Sessions", async () => { expect(attachment.displayName).toBe("attached-file.txt"); expect(attachment.path).toBe(filePath); expect(attachment.lineRange).toEqual({ start: 1, end: 1 }); - - await session.disconnect(); }); it("should send with directory attachment", async () => { @@ -736,7 +711,7 @@ describe("Sessions", async () => { await mkdir(directoryPath, { recursive: true }); await writeFile(`${directoryPath}/readme.txt`, "DIRECTORY_ATTACHMENT_SENTINEL"); - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); await session.sendAndWait({ prompt: "List the attached directory.", @@ -759,8 +734,6 @@ describe("Sessions", async () => { expect(attachment.type).toBe("directory"); expect(attachment.displayName).toBe("attached-directory"); expect(attachment.path).toBe(directoryPath); - - await session.disconnect(); }); it("should send with selection attachment", async () => { @@ -768,7 +741,7 @@ describe("Sessions", async () => { const { writeFile } = await import("fs/promises"); await writeFile(filePath, 'class C { string Value = "SELECTION_SENTINEL"; }'); - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); await session.sendAndWait({ prompt: "Summarize the selected code.", @@ -808,8 +781,6 @@ describe("Sessions", async () => { expect(attachment.text).toBe('string Value = "SELECTION_SENTINEL";'); expect(attachment.selection.start).toEqual({ line: 1, character: 10 }); expect(attachment.selection.end).toEqual({ line: 1, character: 45 }); - - await session.disconnect(); }); it("should accept blob attachments", async () => { @@ -818,7 +789,7 @@ describe("Sessions", async () => { const { writeFile } = await import("fs/promises"); await writeFile(`${workDir}/test-pixel.png`, Buffer.from(pngBase64, "base64")); - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); await session.sendAndWait({ prompt: "Describe this image", @@ -831,12 +802,10 @@ describe("Sessions", async () => { }, ], }); - - await session.disconnect(); }); it("should send with github reference attachment", async () => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); await session.sendAndWait({ prompt: "Using only the GitHub reference metadata in this message, summarize the reference. Do not call any tools.", @@ -876,12 +845,10 @@ describe("Sessions", async () => { expect(attachment.state).toBe("open"); expect(attachment.title).toBe("Add E2E attachment coverage"); expect(attachment.url).toBe("https://github.com/github/copilot-sdk/issues/1234"); - - await session.disconnect(); }); it("should send with mode property", async () => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); await session.sendAndWait({ prompt: "Say mode ok.", @@ -895,12 +862,10 @@ describe("Sessions", async () => { expect(userMessage).toBeDefined(); expect(userMessage!.data.content).toBe("Say mode ok."); expect(userMessage!.data.agentMode).toBe("plan"); - - await session.disconnect(); }); it("should send with custom requestHeaders", async () => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); await session.sendAndWait({ prompt: "What is 1+1?", @@ -919,8 +884,6 @@ describe("Sessions", async () => { const headerValue = headers[matchingKey!]; const headerStr = Array.isArray(headerValue) ? headerValue.join(",") : (headerValue ?? ""); expect(headerStr).toContain("ts-request-headers"); - - await session.disconnect(); }); }); @@ -933,10 +896,8 @@ function getSystemMessage(exchange: ParsedHttpExchange): string | undefined { describe("Send Blocking Behavior", async () => { // Tests for Issue #17: send() should return immediately, not block until turn completes - const { copilotClient: client } = await createSdkTestContext(); - it("send returns immediately while events stream in background", async () => { - const session = await client.createSession({ + await using session = await client.createSession({ onPermissionRequest: approveAll, }); @@ -960,7 +921,7 @@ describe("Send Blocking Behavior", async () => { }); it("sendAndWait blocks until session.idle and returns final assistant message", async () => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); const events: string[] = []; session.on((event) => { @@ -979,16 +940,17 @@ describe("Send Blocking Behavior", async () => { // This test validates client-side timeout behavior. // The snapshot has no assistant response since we expect timeout before completion. it("sendAndWait throws on timeout", async () => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); // Use a slow command to ensure timeout triggers before completion await expect( session.sendAndWait({ prompt: "Run 'sleep 2 && echo done'" }, 100) ).rejects.toThrow(/Timeout after 100ms/); + await session.abort(); }); it("should set model on existing session", async () => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); // Subscribe for the model change event before calling setModel. const modelChangePromise = getNextEventOfType(session, "session.model_change"); @@ -998,12 +960,10 @@ describe("Send Blocking Behavior", async () => { // Verify a model_change event was emitted with the new model. const event = await modelChangePromise; expect(event.data.newModel).toBe("gpt-4.1"); - - await session.disconnect(); }); it("should set model with reasoningEffort", async () => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + await using session = await client.createSession({ onPermissionRequest: approveAll }); const modelChangePromise = getNextEventOfType(session, "session.model_change"); diff --git a/nodejs/test/e2e/session_fs.e2e.test.ts b/nodejs/test/e2e/session_fs.e2e.test.ts index cba98996ef..11de9582e1 100644 --- a/nodejs/test/e2e/session_fs.e2e.test.ts +++ b/nodejs/test/e2e/session_fs.e2e.test.ts @@ -108,7 +108,7 @@ describe("Session Fs", async () => { connection: RuntimeConnection.forTcp({ connectionToken: tcpConnectionToken }), env, }); - onTestFinished(() => client.forceStop()); + onTestFinished(() => client.stop()); await client.createSession({ onPermissionRequest: approveAll, createSessionFsProvider }); const { runtimePort: port } = client as unknown as { runtimePort: number }; @@ -123,7 +123,7 @@ describe("Session Fs", async () => { }), sessionFs: sessionFsConfig, }); - onTestFinished(() => client2.forceStop()); + onTestFinished(() => client2.stop()); await expect(client2.start()).rejects.toThrow(); }); diff --git a/nodejs/test/e2e/streaming_fidelity.e2e.test.ts b/nodejs/test/e2e/streaming_fidelity.e2e.test.ts index 52f893469a..17b5222616 100644 --- a/nodejs/test/e2e/streaming_fidelity.e2e.test.ts +++ b/nodejs/test/e2e/streaming_fidelity.e2e.test.ts @@ -3,11 +3,11 @@ *--------------------------------------------------------------------------------------------*/ import { describe, expect, it, onTestFinished } from "vitest"; -import { CopilotClient, SessionEvent, approveAll } from "../../src/index.js"; +import { SessionEvent, approveAll } from "../../src/index.js"; import { createSdkTestContext, isCI } from "./harness/sdkTestContext"; describe("Streaming Fidelity", async () => { - const { copilotClient: client, env } = await createSdkTestContext(); + const { copilotClient: client, createClient } = await createSdkTestContext(); it("should produce delta events when streaming is enabled", async () => { const session = await client.createSession({ @@ -81,11 +81,10 @@ describe("Streaming Fidelity", async () => { await session.disconnect(); // Resume using a new client - const newClient = new CopilotClient({ - env, + const newClient = createClient({ gitHubToken: isCI ? "fake-token-for-e2e-tests" : process.env.GITHUB_TOKEN, }); - onTestFinished(() => newClient.forceStop()); + onTestFinished(() => newClient.stop()); const session2 = await newClient.resumeSession(session.sessionId, { onPermissionRequest: approveAll, streaming: true, @@ -120,11 +119,10 @@ describe("Streaming Fidelity", async () => { await session.disconnect(); // Resume using a new client with streaming DISABLED - const newClient = new CopilotClient({ - env, + const newClient = createClient({ gitHubToken: isCI ? "fake-token-for-e2e-tests" : process.env.GITHUB_TOKEN, }); - onTestFinished(() => newClient.forceStop()); + onTestFinished(() => newClient.stop()); const session2 = await newClient.resumeSession(session.sessionId, { onPermissionRequest: approveAll, streaming: false, diff --git a/nodejs/test/e2e/subagent_hooks.e2e.test.ts b/nodejs/test/e2e/subagent_hooks.e2e.test.ts index ac0a694dca..dbc3ca673b 100644 --- a/nodejs/test/e2e/subagent_hooks.e2e.test.ts +++ b/nodejs/test/e2e/subagent_hooks.e2e.test.ts @@ -6,20 +6,79 @@ import { writeFile } from "fs/promises"; import { join } from "path"; import { describe, expect, it } from "vitest"; import type { + CopilotRequestContext, PreToolUseHookInput, PreToolUseHookOutput, PostToolUseHookInput, PostToolUseHookOutput, } from "../../src/index.js"; -import { approveAll } from "../../src/index.js"; +import { approveAll, CopilotRequestHandler } from "../../src/index.js"; import { createSdkTestContext, isCI } from "./harness/sdkTestContext.js"; +interface RequestRecord { + url: string; + agentId?: string; + parentAgentId?: string; + interactionType?: string; +} + +class RecordingRequestHandler extends CopilotRequestHandler { + readonly records: RequestRecord[] = []; + + protected override async sendRequest( + request: Request, + ctx: CopilotRequestContext + ): Promise { + this.records.push({ + url: request.url, + agentId: ctx.agentId, + parentAgentId: ctx.parentAgentId, + interactionType: ctx.interactionType, + }); + return super.sendRequest(request, ctx); + } +} + +function isInferenceUrl(url: string): boolean { + const u = url.toLowerCase(); + return ( + u.endsWith("/chat/completions") || + u.endsWith("/responses") || + u.endsWith("/v1/messages") || + u.endsWith("/messages") + ); +} + +function expectSubagentRequestMetadata(records: RequestRecord[]): void { + const inference = records.filter((r) => isInferenceUrl(r.url)); + expect(inference.length, "request handler should observe inference requests").toBeGreaterThan( + 0 + ); + + const subagentRequest = inference.find((r) => r.parentAgentId); + expect( + subagentRequest, + "sub-agent inference request should carry a parentAgentId" + ).toBeDefined(); + expect( + subagentRequest!.agentId, + "sub-agent inference request should carry an agentId" + ).toBeTruthy(); + expect( + subagentRequest!.interactionType, + "sub-agent inference request should carry an interactionType" + ).toBeTruthy(); + expect(subagentRequest!.parentAgentId).not.toBe(subagentRequest!.agentId); +} + describe("Subagent hooks", async () => { // For snapshot recording (non-CI), use RECORD_GH_TOKEN if available const recordToken = !isCI ? process.env.RECORD_GH_TOKEN : undefined; + const requestHandler = new RecordingRequestHandler(); const { copilotClient: client, workDir } = await createSdkTestContext({ copilotClientOptions: { ...(recordToken ? { gitHubToken: recordToken } : {}), + requestHandler, env: { COPILOT_EXP_COPILOT_CLI_SESSION_BASED_SUBAGENTS: "true" }, }, }); @@ -75,6 +134,7 @@ describe("Subagent hooks", async () => { // input.sessionId distinguishes parent from sub-agent: parent tools and // sub-agent tools carry different sessionIds expect(viewPre[0].sessionId).not.toBe(taskPre!.sessionId); + expectSubagentRequestMetadata(requestHandler.records); await session.disconnect(); }, 120_000); diff --git a/nodejs/test/e2e/suspend.e2e.test.ts b/nodejs/test/e2e/suspend.e2e.test.ts index 79e8987260..2c8639ad38 100644 --- a/nodejs/test/e2e/suspend.e2e.test.ts +++ b/nodejs/test/e2e/suspend.e2e.test.ts @@ -4,8 +4,8 @@ import { describe, expect, it, onTestFinished } from "vitest"; import { z } from "zod"; -import { approveAll, CopilotClient, defineTool, RuntimeConnection } from "../../src/index.js"; import type { PermissionRequest, PermissionRequestResult, SessionEvent } from "../../src/index.js"; +import { approveAll, CopilotClient, defineTool, RuntimeConnection } from "../../src/index.js"; import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; const SUSPEND_TIMEOUT_MS = 60_000; @@ -47,10 +47,10 @@ async function waitWithTimeout( } } -function onTestFinishedForceStop(client: CopilotClient): void { +function onTestFinishedStop(client: CopilotClient): void { onTestFinished(async () => { try { - await client.forceStop(); + await client.stop(); } catch { // Ignore cleanup errors } @@ -71,7 +71,7 @@ describe("Suspend RPC", async () => { connectionToken: SHARED_TOKEN, }), }); - onTestFinishedForceStop(server); + onTestFinishedStop(server); return server; } @@ -79,7 +79,7 @@ describe("Suspend RPC", async () => { const connectedClient = new CopilotClient({ connection: RuntimeConnection.forUri(cliUrl, { connectionToken: SHARED_TOKEN }), }); - onTestFinishedForceStop(connectedClient); + onTestFinishedStop(connectedClient); return connectedClient; } diff --git a/nodejs/test/e2e/telemetry.e2e.test.ts b/nodejs/test/e2e/telemetry.e2e.test.ts index 9df6b7f88e..c0f71ebfc6 100644 --- a/nodejs/test/e2e/telemetry.e2e.test.ts +++ b/nodejs/test/e2e/telemetry.e2e.test.ts @@ -6,7 +6,7 @@ import { readFile } from "fs/promises"; import { join } from "path"; import { describe, expect, it } from "vitest"; import { z } from "zod"; -import { approveAll, defineTool } from "../../src/index.js"; +import { approveAll, defineTool, RuntimeConnection } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; import { getFinalAssistantMessage } from "./harness/sdkTestHelper.js"; @@ -58,6 +58,12 @@ describe("Telemetry export", async () => { const { copilotClient: client, workDir } = await createSdkTestContext({ copilotClientOptions: { + // Telemetry is lowered to environment variables the native runtime reads, which + // the in-process transport cannot carry per-client (the runtime runs in the shared + // host process); see https://github.com/github/copilot-sdk/issues/1934. Pin the + // child-process (stdio) transport so this scenario is exercised even in the + // in-process CI cell, matching the .NET suite. + connection: RuntimeConnection.forStdio(), telemetry: { filePath: telemetryFileName, exporterType: "file", diff --git a/nodejs/test/e2e/ui_elicitation.e2e.test.ts b/nodejs/test/e2e/ui_elicitation.e2e.test.ts index 3bc9335a21..2e85dd5af2 100644 --- a/nodejs/test/e2e/ui_elicitation.e2e.test.ts +++ b/nodejs/test/e2e/ui_elicitation.e2e.test.ts @@ -5,7 +5,7 @@ import { afterAll, describe, expect, it } from "vitest"; import { CopilotClient, approveAll, RuntimeConnection } from "../../src/index.js"; import type { SessionEvent } from "../../src/index.js"; -import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { createSdkTestContext, isInProcessTransport } from "./harness/sdkTestContext.js"; describe("UI Elicitation", async () => { const { copilotClient: client } = await createSdkTestContext(); @@ -116,7 +116,7 @@ describe("UI Elicitation Multi-Client Capabilities", async () => { } ); - it( + it.skipIf(isInProcessTransport)( "capabilities.changed fires when elicitation provider disconnects", { timeout: 60_000 }, async () => { diff --git a/nodejs/test/session-event-codegen.test.ts b/nodejs/test/session-event-codegen.test.ts index 86c76f71bc..14340292be 100644 --- a/nodejs/test/session-event-codegen.test.ts +++ b/nodejs/test/session-event-codegen.test.ts @@ -209,6 +209,38 @@ describe("session event codegen", () => { ); }); + it("drops leading underscores from C# member names while preserving JSON names", () => { + const schema: JSONSchema7 = { + definitions: { + SessionEvent: { + anyOf: [ + { + type: "object", + required: ["type", "data"], + properties: { + type: { const: "session.synthetic" }, + data: { + type: "object", + required: ["_meta"], + properties: { + _meta: { type: "string" }, + }, + }, + }, + }, + ], + }, + }, + }; + + const csharpCode = generateCSharpSessionEventsCode(schema); + + expect(csharpCode).toContain( + '[JsonPropertyName("_meta")]\n public required string Meta { get; set; }' + ); + expect(csharpCode).not.toContain("public required string _meta"); + }); + it("collapses redundant callable wrapper lambdas", () => { const schema: JSONSchema7 = { definitions: { diff --git a/python/README.md b/python/README.md index 86f568bd7a..3089c36699 100644 --- a/python/README.md +++ b/python/README.md @@ -32,6 +32,17 @@ python -m copilot download-runtime This caches the runtime binary locally. If you skip this step, the SDK will attempt to download it automatically on first use as a fallback. +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`: + +```bash +python -m copilot download-runtime --in-process +``` + +This additionally fetches the native runtime library into the versioned runtime +cache. Stdio/TCP users never download it. When omitted, it is downloaded +lazily on first use of the in-process transport. + | Platform | Cache path | |----------|-----------| | Linux | `~/.cache/github-copilot-sdk/cli//copilot` | @@ -136,7 +147,7 @@ asyncio.run(main()) ## Features - ✅ Full JSON-RPC protocol support -- ✅ stdio and TCP transports +- ✅ stdio, TCP, and in-process (FFI) transports - ✅ Real-time streaming events - ✅ Session history with `get_events()` - ✅ Type hints throughout @@ -184,8 +195,9 @@ CopilotClient(connection=..., log_level="debug", github_token=..., ...) All options are kw-only parameters: - `connection` (RuntimeConnection | None): How to reach the runtime. Use - `RuntimeConnection.for_stdio(...)`, `RuntimeConnection.for_tcp(...)`, or - `RuntimeConnection.for_uri(...)`. Defaults to a stdio connection with the bundled binary. + `RuntimeConnection.for_stdio(...)`, `RuntimeConnection.for_tcp(...)`, + `RuntimeConnection.for_uri(...)`, or `RuntimeConnection.for_inprocess(...)`. + Defaults to a stdio connection with the bundled binary. - `working_directory` (str | None): Working directory for the CLI process (default: current dir). - `log_level` (str): Log level (default: "info"). - `env` (dict | None): Environment variables for the CLI process. @@ -204,6 +216,51 @@ All options are kw-only parameters: - `RuntimeConnection.for_stdio(path=None, args=None)` — spawn a local CLI process and talk over stdio. - `RuntimeConnection.for_tcp(port=0, connection_token=None, path=None, args=None)` — spawn a local CLI in TCP mode. - `RuntimeConnection.for_uri(url, connection_token=None)` — connect to an existing CLI server (e.g. `"localhost:8080"`). +- `RuntimeConnection.for_inprocess()` — host the runtime in-process via its native C ABI (FFI). See [In-process (FFI) transport](#in-process-ffi-transport). + +Child-process connections (`for_stdio`/`for_tcp`) also expose a per-connection +`env` field for the spawned process. Set it on the returned connection instead of +the client-level `env` — setting both raises: + +```python +conn = RuntimeConnection.for_stdio() +conn.env = {"MY_VAR": "value"} +client = CopilotClient(connection=conn) # do NOT also pass env=... here +``` + +### In-process (FFI) transport + +> ⚠️ **Experimental.** The in-process transport loads the runtime's native shared +> library into your process and drives JSON-RPC over its C ABI (via stdlib +> `ctypes`), instead of spawning a child process. + +```python +from copilot import CopilotClient, RuntimeConnection + +client = CopilotClient(connection=RuntimeConnection.for_inprocess()) +await client.start() +try: + pong = await client.ping("hello") + print(pong.message) +finally: + await client.stop() +``` + +**Requirements & behavior:** + +- Pre-provision the native runtime with + `python -m copilot download-runtime --in-process`, or let the SDK download it + lazily on first use of this transport. +- Set `COPILOT_CLI_PATH` only when using an externally provisioned compatible + runtime package. In-process connections do not accept per-connection paths + or raw process arguments. +- Because the runtime shares this single host process, per-client options that + lower to environment variables or a working directory **cannot** be honored and + are rejected: `env`, `telemetry`, and `working_directory` all raise `ValueError` + with `for_inprocess()`. Set the corresponding values on the host process + environment / working directory before creating the client instead. +- Set `COPILOT_SDK_DEFAULT_CONNECTION=inprocess` to select the in-process + transport by default when no explicit `connection` is supplied. **`CopilotClient.create_session()`:** diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index fdf422d2aa..76f79b79f4 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -24,6 +24,7 @@ CanvasHostContext, CanvasHostContextCapabilities, CanvasJsonSchema, + CanvasProviderIdentity, ExtensionInfo, OpenCanvasInstance, ) @@ -35,6 +36,7 @@ CopilotClient, GetAuthStatusResponse, GetStatusResponse, + InProcessRuntimeConnection, LogLevel, ModelBilling, ModelCapabilities, @@ -74,6 +76,7 @@ LlmInferenceHeaders, ) from .generated.rpc import ( + CurrentToolMetadata, GitHubTelemetryClientInfo, GitHubTelemetryEvent, GitHubTelemetryNotification, @@ -156,6 +159,7 @@ SessionUiApi, SessionUiCapabilities, SystemMessageConfig, + ToolSearchConfig, UserInputHandler, UserInputRequest, UserInputResponse, @@ -200,6 +204,7 @@ "CanvasHostContext", "CanvasHostContextCapabilities", "CanvasJsonSchema", + "CanvasProviderIdentity", "CapiSessionOptions", "ChildProcessRuntimeConnection", "CloudSessionOptions", @@ -214,6 +219,7 @@ "CopilotWebSocketCloseStatus", "CopilotWebSocketHandler", "CreateSessionFsHandler", + "CurrentToolMetadata", "ElicitationContext", "ElicitationHandler", "ElicitationParams", @@ -233,6 +239,7 @@ "GitHubTelemetryEvent", "GitHubTelemetryNotification", "InfiniteSessionConfig", + "InProcessRuntimeConnection", "InputOptions", "LargeToolOutputConfig", "LlmInferenceHeaders", @@ -330,6 +337,7 @@ "ToolInvocation", "ToolResult", "ToolResultType", + "ToolSearchConfig", "ToolSet", "UriRuntimeConnection", "UserInputHandler", diff --git a/python/copilot/_cli_download.py b/python/copilot/_cli_download.py index 1a5ebc9024..b831e072ad 100644 --- a/python/copilot/_cli_download.py +++ b/python/copilot/_cli_download.py @@ -16,6 +16,7 @@ from __future__ import annotations +import base64 import hashlib import io import os @@ -35,6 +36,9 @@ get_asset_info, get_checksums_url, get_download_url, + get_npm_platform, + get_runtime_lib_packument_url, + get_runtime_lib_url, ) _CACHE_DIR_NAME = "github-copilot-sdk" @@ -304,6 +308,153 @@ def download_cli(version: str | None = None, *, force: bool = False) -> str: return str(binary_path) +def _fetch_url_bytes(url: str, *, timeout: int) -> bytes: + """Download bytes from ``url`` with retries.""" + last_exc: Exception | None = None + for attempt in range(_MAX_RETRIES): + try: + with urlopen(url, timeout=timeout) as response: + return response.read() + except (HTTPError, URLError) 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 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``). + + 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. + + 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 + download/verification failure. + """ + # Import lazily to avoid a hard dependency for stdio-only users. + from ._ffi_runtime_host import _natural_library_name, resolve_library_path + + # Already present (bundled prebuilds layout in dev, or a prior download)? + existing = resolve_library_path(cli_path) + 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() + except RuntimeError: + return None + + cli_dir = Path(cli_path).resolve().parent + lib_path = cli_dir / _natural_library_name() + 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) + + # 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. + 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) + os.replace(tmp_name, lib_path) + except OSError: + try: + os.unlink(tmp_name) + except OSError: + # Best-effort cleanup of the temp file; ignore if it's already gone or + # can't be removed (the OS reclaims it, and it doesn't affect correctness). + pass + if lib_path.exists(): + return str(lib_path) + raise + + return str(lib_path) + + def get_or_download_cli(version: str | None = None) -> str | None: """Get the cached CLI binary, downloading it if necessary. @@ -361,6 +512,15 @@ def main() -> None: "--version", help="Runtime version to download (default: pinned version)", ) + dl_parser.add_argument( + "--in-process", + action="store_true", + help=( + "Also download the native in-process (FFI) runtime library " + "(prebuilds//runtime.node) and place it next to the CLI. " + "Only needed for the experimental in-process transport." + ), + ) args = parser.parse_args() @@ -378,6 +538,17 @@ def main() -> None: try: path = download_cli(ver, force=args.force) print(f"Runtime cached at: {path}") + if args.in_process: + print("Downloading in-process (FFI) runtime library...") + lib_path = ensure_runtime_library(path, ver) + if lib_path: + print(f"Runtime library cached at: {lib_path}") + else: + print( + "Warning: could not provision the in-process runtime library " + "(download disabled or unsupported platform).", + file=sys.stderr, + ) except RuntimeError as exc: print(f"Error: {exc}", file=sys.stderr) sys.exit(1) diff --git a/python/copilot/_cli_version.py b/python/copilot/_cli_version.py index 4ff9cfb7af..cb5939820a 100644 --- a/python/copilot/_cli_version.py +++ b/python/copilot/_cli_version.py @@ -36,6 +36,31 @@ _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] = { + ("linux", "x86_64"): "linux-x64", + ("linux", "aarch64"): "linux-arm64", + ("linux", "arm64"): "linux-arm64", + ("darwin", "x86_64"): "darwin-x64", + ("darwin", "arm64"): "darwin-arm64", + ("win32", "AMD64"): "win32-x64", + ("win32", "ARM64"): "win32-arm64", +} + +# Musl (Alpine) npm platform variants — detected at runtime via _is_musl(). +_MUSL_NPM_PLATFORMS: dict[str, str] = { + "x86_64": "linuxmusl-x64", + "aarch64": "linuxmusl-arm64", + "arm64": "linuxmusl-arm64", +} + def _is_musl() -> bool: """Detect whether the current Linux system uses musl libc (e.g. Alpine).""" @@ -93,3 +118,45 @@ def get_checksums_url(version: str) -> str: base = os.environ.get("COPILOT_CLI_DOWNLOAD_BASE_URL", _DOWNLOAD_BASE_URL) 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. + + Used to locate the native in-process runtime library. 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]) + if musl: + return musl + + npm_platform = NPM_PLATFORMS.get(key) + if npm_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)}" + ) + return npm_platform + + +def get_runtime_lib_packument_url(npm_platform: str) -> str: + """Return the npm packument URL for the platform runtime package.""" + import os + + 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" diff --git a/python/copilot/_ffi_runtime_host.py b/python/copilot/_ffi_runtime_host.py new file mode 100644 index 0000000000..e04d1655e6 --- /dev/null +++ b/python/copilot/_ffi_runtime_host.py @@ -0,0 +1,514 @@ +"""In-process (FFI) hosting of the Copilot runtime. + +Instead of spawning the Copilot CLI as a child process and talking JSON-RPC over +stdio/TCP, the in-process transport loads the runtime's native shared library +(``runtime.node`` — a Rust ``cdylib``) into this process and drives JSON-RPC over +its C ABI (FFI). The native ``host_start`` export spawns the residual worker +itself, so the SDK never launches the worker directly; it only pumps opaque LSP +``Content-Length:``-framed JSON-RPC bytes across the boundary: + +- client → server frames go to ``copilot_runtime_connection_write`` +- server → client frames arrive on a native callback that feeds a thread-safe + receive buffer + +The existing :class:`~copilot._jsonrpc.JsonRpcClient` handles framing unchanged — +this is a transport swap, not a new protocol. The host exposes a *process-like* +adapter (``stdin``/``stdout``/``stderr``/``poll``) so ``JsonRpcClient`` can drive +it exactly like a :class:`subprocess.Popen`. + +The C ABI (shared with the .NET, Node.js, and Rust SDKs):: + + uint32 copilot_runtime_host_start(uint8 *argv, size_t argv_len, + uint8 *env, size_t env_len); + bool copilot_runtime_host_shutdown(uint32 server_id); + uint32 copilot_runtime_connection_open(uint32 server_id, outbound cb, + void *user_data, + uint8 *a, size_t a_len, + uint8 *b, size_t b_len, + uint8 *c, size_t c_len); + bool copilot_runtime_connection_write(uint32 conn_id, + uint8 *bytes, size_t len); + bool copilot_runtime_connection_close(uint32 conn_id); + // outbound callback: + void outbound(void *user_data, uint8 *bytes, size_t len); +""" + +from __future__ import annotations + +import ctypes +import json +import logging +import os +import sys +import threading +import time +from collections.abc import Sequence +from pathlib import Path + +logger = logging.getLogger("copilot.ffi") + +_SYMBOL_PREFIX = "copilot_runtime_" + +# The C ABI outbound callback: void(void *user_data, uint8 *bytes, size_t len). +_OutboundCallback = ctypes.CFUNCTYPE( + None, ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t +) + + +def get_prebuilds_folder() -> str | None: + """Return the ``prebuilds/`` folder name for the current host. + + Matches the napi-rs ``-`` layout the runtime package + ships (e.g. ``linux-x64``, ``darwin-arm64``, ``win32-x64``), including the + musl (Alpine) variants. Returns ``None`` for unsupported platforms. + """ + if sys.platform.startswith("linux"): + platform_name = "linuxmusl" if _is_musl() else "linux" + elif sys.platform == "darwin": + platform_name = "darwin" + elif sys.platform == "win32": + platform_name = "win32" + else: + return None + + machine = _normalize_machine() + if machine is None: + return None + return f"{platform_name}-{machine}" + + +def _normalize_machine() -> str | None: + import platform + + machine = platform.machine().lower() + if machine in ("x86_64", "amd64", "x64"): + return "x64" + if machine in ("arm64", "aarch64"): + return "arm64" + return None + + +def _is_musl() -> bool: + """Detect whether the current Linux system uses musl libc (e.g. Alpine).""" + if sys.platform != "linux": + return False + try: + import subprocess + + result = subprocess.run(["ldd", "--version"], capture_output=True, text=True, timeout=5) + return "musl" in (result.stdout + result.stderr).lower() + except (FileNotFoundError, OSError, Exception): # noqa: BLE001 + return False + + +def _natural_library_name() -> str: + """The natural platform shared-library file name for the runtime cdylib. + + The ``.node`` file renamed to what a Rust ``cdylib`` would be called on this + OS. The library is loaded by absolute path, so the on-disk name is ours. + """ + if sys.platform == "win32": + return "copilot_runtime.dll" + if sys.platform == "darwin": + return "libcopilot_runtime.dylib" + return "libcopilot_runtime.so" + + +def resolve_library_path(cli_entrypoint: str) -> str | None: + """Resolve the native runtime library next to the given CLI entrypoint. + + Checks, in order: + + 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). + + Returns the absolute path, or ``None`` when neither exists. + """ + directory = Path(cli_entrypoint).resolve().parent + + flat = directory / _natural_library_name() + if flat.is_file(): + return str(flat) + + folder = get_prebuilds_folder() + if folder is not None: + prebuilt = directory / "prebuilds" / folder / "runtime.node" + if prebuilt.is_file(): + return str(prebuilt) + + return None + + +# The cdylib may only be loaded once per process; a second load of a *different* +# path is unsupported (matches the Node/Rust hosts). Guard it here. +_loaded_library: ctypes.CDLL | None = None +_loaded_library_path: str | None = None +_load_lock = threading.Lock() + + +class _FfiLibrary: + """Binds the ``copilot_runtime_*`` C ABI exports of a loaded cdylib.""" + + def __init__(self, lib: ctypes.CDLL) -> None: + self._lib = lib + + self.host_start = getattr(lib, f"{_SYMBOL_PREFIX}host_start") + self.host_start.argtypes = [ + ctypes.c_char_p, + ctypes.c_size_t, + ctypes.c_char_p, + ctypes.c_size_t, + ] + self.host_start.restype = ctypes.c_uint32 + + self.host_shutdown = getattr(lib, f"{_SYMBOL_PREFIX}host_shutdown") + self.host_shutdown.argtypes = [ctypes.c_uint32] + self.host_shutdown.restype = ctypes.c_bool + + self.connection_open = getattr(lib, f"{_SYMBOL_PREFIX}connection_open") + self.connection_open.argtypes = [ + ctypes.c_uint32, + _OutboundCallback, + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_size_t, + ctypes.c_char_p, + ctypes.c_size_t, + ctypes.c_char_p, + ctypes.c_size_t, + ] + self.connection_open.restype = ctypes.c_uint32 + + self.connection_write = getattr(lib, f"{_SYMBOL_PREFIX}connection_write") + self.connection_write.argtypes = [ + ctypes.c_uint32, + ctypes.c_char_p, + ctypes.c_size_t, + ] + self.connection_write.restype = ctypes.c_bool + + self.connection_close = getattr(lib, f"{_SYMBOL_PREFIX}connection_close") + self.connection_close.argtypes = [ctypes.c_uint32] + self.connection_close.restype = ctypes.c_bool + + +def _load_library(library_path: str) -> _FfiLibrary: + global _loaded_library, _loaded_library_path + with _load_lock: + if _loaded_library is not None: + if _loaded_library_path != library_path: + raise RuntimeError( + f"An in-process FFI runtime library is already loaded from " + f"'{_loaded_library_path}'; loading a different library from " + f"'{library_path}' in the same process is not supported." + ) + 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; + # eager binding surfaces any load problem here rather than at first call. + if sys.platform == "win32": + lib = ctypes.WinDLL(library_path) + else: + lib = ctypes.CDLL(library_path, mode=os.RTLD_NOW | os.RTLD_LOCAL) + _loaded_library = lib + _loaded_library_path = library_path + return _FfiLibrary(lib) + + +class _ReceiveBuffer: + """Thread-safe byte buffer feeding blocking ``read(n)`` from a producer thread. + + The native outbound callback (invoked on a foreign runtime thread) appends + frames via :meth:`feed` without ever blocking; the JSON-RPC reader thread + drains them via :meth:`read`, which blocks until data or EOF. + """ + + def __init__(self) -> None: + self._buffer = bytearray() + self._closed = False + self._cond = threading.Condition() + + def feed(self, data: bytes) -> None: + with self._cond: + if self._closed: + return + self._buffer.extend(data) + self._cond.notify_all() + + def close(self) -> None: + with self._cond: + self._closed = True + self._cond.notify_all() + + def read(self, size: int) -> bytes: + if size <= 0: + return b"" + with self._cond: + while not self._buffer and not self._closed: + self._cond.wait() + if not self._buffer: + return b"" # EOF + chunk = bytes(self._buffer[:size]) + del self._buffer[:size] + return chunk + + def readline(self) -> bytes: + """Read through the next ``\\n`` (inclusive), blocking until available. + + Returns whatever remains (possibly without a trailing newline) at EOF, or + ``b""`` if the buffer is empty and closed. Mirrors the blocking + ``BufferedReader.readline`` semantics :class:`JsonRpcClient` expects when + parsing LSP ``Content-Length:`` headers. + """ + with self._cond: + while b"\n" not in self._buffer and not self._closed: + self._cond.wait() + newline_index = self._buffer.find(b"\n") + if newline_index == -1: + # EOF with no newline: return the remaining bytes (may be empty). + line = bytes(self._buffer) + self._buffer.clear() + return line + end = newline_index + 1 + line = bytes(self._buffer[:end]) + del self._buffer[:end] + return line + + +class _FfiStdin: + """Writable side of the process-like adapter; forwards frames to the runtime.""" + + def __init__(self, host: FfiRuntimeHost) -> None: + self._host = host + + def write(self, data: bytes) -> int: + self._host._write_frame(data) + return len(data) + + def flush(self) -> None: + # connection_write enqueues synchronously, so there is nothing to flush. + pass + + +class _FfiProcessAdapter: + """A ``subprocess.Popen``-shaped view over an :class:`FfiRuntimeHost`. + + :class:`~copilot._jsonrpc.JsonRpcClient` only needs ``stdin`` (writable), + ``stdout`` (blocking ``read``), an optional ``stderr``, and ``poll()``. The + in-process transport has no OS pipes, so this adapter bridges those to the + FFI host's frame plumbing. + """ + + def __init__(self, host: FfiRuntimeHost) -> None: + self._host = host + self.stdin = _FfiStdin(host) + self.stdout = host._receive_buffer + # No separate error stream in-process; JsonRpcClient skips the stderr + # thread when this is falsy. + self.stderr = None + + def poll(self) -> int | None: + """Return ``None`` while the connection is live, ``0`` once closed.""" + return None if not self._host._disposed else 0 + + def terminate(self) -> None: + self._host.dispose() + + def kill(self) -> None: + self._host.dispose() + + def wait(self, timeout: float | None = None) -> int: # noqa: ARG002 + self._host.dispose() + return 0 + + +class FfiRuntimeHost: + """Hosts the Copilot runtime in-process via its native C ABI. + + Construct with :meth:`create`, then :meth:`start` to spawn the worker and open + the FFI connection. Expose :attr:`process` to :class:`JsonRpcClient`, and call + :meth:`dispose` to tear everything down. + """ + + def __init__( + self, + library_path: str, + cli_entrypoint: str, + environment: dict[str, str] | None = None, + args: Sequence[str] = (), + ) -> None: + self._library_path = library_path + self._cli_entrypoint = cli_entrypoint + self._environment = environment + self._extra_args = list(args) + self._lib = _load_library(library_path) + + self._server_id = 0 + self._connection_id = 0 + self._disposed = False + self._dispose_lock = threading.Lock() + + self._receive_buffer = _ReceiveBuffer() + # Keep a strong reference to the ctypes callback for its whole lifetime; + # dropping it while native code can still invoke it is a use-after-free. + self._outbound_callback: ctypes._FuncPointer | None = None + # Serializes teardown against in-flight native callbacks. + self._active_callbacks = 0 + self._callback_lock = threading.Lock() + + self._process = _FfiProcessAdapter(self) + + @property + def process(self) -> _FfiProcessAdapter: + """The ``subprocess.Popen``-shaped adapter for :class:`JsonRpcClient`.""" + return self._process + + @staticmethod + def create( + cli_entrypoint: str, + environment: dict[str, str] | None = None, + args: Sequence[str] = (), + ) -> FfiRuntimeHost: + """Resolve the cdylib next to the CLI entrypoint and prepare the host. + + Raises: + RuntimeError: If the native runtime library cannot be found. + """ + full_entrypoint = str(Path(cli_entrypoint).resolve()) + library_path = resolve_library_path(full_entrypoint) + if library_path is None: + raise RuntimeError( + "In-process FFI runtime library not found next to " + f"'{full_entrypoint}'. Download it with " + "`python -m copilot download-runtime --in-process`, or set " + "COPILOT_CLI_PATH to a runtime package that ships it." + ) + return FfiRuntimeHost(library_path, full_entrypoint, environment, args) + + def _build_argv(self) -> bytes: + # A `.js` entrypoint (dev) is launched via node; the packaged single-file + # CLI embeds its own Node and is invoked directly. `--no-auto-update` + # pins the worker to the runtime package matching the loaded cdylib. + if self._cli_entrypoint.lower().endswith(".js"): + argv = ["node", self._cli_entrypoint, "--embedded-host", "--no-auto-update"] + else: + argv = [self._cli_entrypoint, "--embedded-host", "--no-auto-update"] + argv.extend(self._extra_args) + return json.dumps(argv).encode("utf-8") + + def _build_env(self) -> bytes | None: + if not self._environment: + return None + obj = {k: v for k, v in self._environment.items() if v is not None} + if not obj: + return None + return json.dumps(obj).encode("utf-8") + + def start_blocking(self) -> None: + """Spawn the worker and open the FFI connection (blocks up to ~30s). + + Must be run off the event loop (e.g. via :func:`asyncio.to_thread`); + ``host_start`` blocks until the worker connects back and signals + readiness. + """ + argv = self._build_argv() + env = self._build_env() + + self._server_id = self._lib.host_start(argv, len(argv), env, len(env) if env else 0) + if not self._server_id: + raise RuntimeError( + f"copilot_runtime_host_start failed (library '{self._library_path}', " + f"entrypoint '{self._cli_entrypoint}')." + ) + + self._outbound_callback = _OutboundCallback(self._on_outbound) + self._connection_id = self._lib.connection_open( + self._server_id, + self._outbound_callback, + None, + None, + 0, + None, + 0, + None, + 0, + ) + if not self._connection_id: + self._outbound_callback = None + self._lib.host_shutdown(self._server_id) + self._server_id = 0 + raise RuntimeError("copilot_runtime_connection_open failed.") + + def _on_outbound( + self, + _user_data: int | None, + bytes_ptr: ctypes._Pointer, + bytes_len: int, + ) -> None: + """Native server → client callback (invoked on a foreign runtime thread). + + The native pointer is only valid for this call, so the bytes are copied + out before returning. Exceptions must not cross the FFI boundary, so + everything is caught and logged. + """ + with self._callback_lock: + if self._disposed: + return + self._active_callbacks += 1 + try: + if bytes_ptr and bytes_len > 0: + data = ctypes.string_at(bytes_ptr, bytes_len) + self._receive_buffer.feed(data) + except Exception: # noqa: BLE001 + logger.error("In-process FFI inbound callback failed", exc_info=True) + finally: + with self._callback_lock: + self._active_callbacks -= 1 + + def _write_frame(self, frame: bytes) -> None: + if self._disposed or not self._connection_id: + raise RuntimeError("The in-process runtime connection is closed.") + ok = self._lib.connection_write(self._connection_id, frame, len(frame)) + if not ok: + raise RuntimeError("Failed to write a frame to the in-process runtime connection.") + + def dispose(self) -> None: + """Close the FFI connection, shut down the native host, release resources. + + Idempotent. Waits for any in-flight outbound callback to finish before + dropping the callback reference to avoid a use-after-free. + """ + with self._dispose_lock: + if self._disposed: + return + self._disposed = True + + # Stop accepting new callbacks and wait for in-flight ones to drain. + with self._callback_lock: + pass # _disposed is set; new callbacks bail out immediately. + while True: + with self._callback_lock: + if self._active_callbacks == 0: + break + time.sleep(0.001) + + try: + if self._connection_id: + self._lib.connection_close(self._connection_id) + self._connection_id = 0 + except Exception: # noqa: BLE001 + logger.debug("Error closing in-process FFI connection", exc_info=True) + + try: + if self._server_id: + self._lib.host_shutdown(self._server_id) + self._server_id = 0 + except Exception: # noqa: BLE001 + logger.debug("Error shutting down in-process FFI host", exc_info=True) + + self._receive_buffer.close() + # Safe to drop now: no native code can invoke the callback after + # connection_close, and all in-flight callbacks have drained. + self._outbound_callback = None diff --git a/python/copilot/canvas.py b/python/copilot/canvas.py index ddbc8539a2..9b8dec5258 100644 --- a/python/copilot/canvas.py +++ b/python/copilot/canvas.py @@ -39,6 +39,7 @@ "CanvasHostContext", "CanvasHostContextCapabilities", "CanvasJsonSchema", + "CanvasProviderIdentity", "ExtensionInfo", "OpenCanvasInstance", ] @@ -66,6 +67,33 @@ def to_dict(self) -> dict[str, Any]: return {"source": self.source, "name": self.name} +@dataclass +class CanvasProviderIdentity: + """Stable identity for a host/SDK connection that supplies built-in canvases. + + Lets a host advertise a stable canvas-provider extension id so host-provided + canvases restore across a cold session resume. Serializes to + ``{"id": ...}`` (with an optional ``"name"``) on the wire. + + .. note:: + + **Experimental.** This type is part of an experimental wire-protocol + surface and may change or be removed in future SDK or CLI releases. + """ + + id: str + """Stable provider identifier, e.g. ``"app:builtin:window-1"``.""" + + name: str | None = None + """Optional human-readable provider name.""" + + def to_dict(self) -> dict[str, Any]: + result: dict[str, Any] = {"id": self.id} + if self.name is not None: + result["name"] = self.name + return result + + @dataclass class CanvasDeclaration: """Declarative metadata for a single canvas, sent on create/resume. diff --git a/python/copilot/client.py b/python/copilot/client.py index 55d01c5b57..e6ac9b03e5 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -32,6 +32,7 @@ from typing import Any, ClassVar, Literal, TypedDict, cast, overload from ._diagnostics import log_timing +from ._ffi_runtime_host import FfiRuntimeHost from ._jsonrpc import JsonRpcClient, JsonRpcError, ProcessExitedError from ._mode import ( CopilotClientMode, @@ -58,6 +59,7 @@ from .canvas import ( CanvasDeclaration, CanvasHandler, + CanvasProviderIdentity, ExtensionInfo, ) from .copilot_request_handler import CopilotRequestHandler, create_copilot_request_adapter @@ -71,6 +73,8 @@ RemoteSessionMode, ServerRpc, _ConnectResult, + _HookInvokeRequest, + _HookInvokeResponse, from_datetime, register_client_global_api_handlers, register_client_session_api_handlers, @@ -106,6 +110,7 @@ SessionHooks, SessionLimitsConfig, SystemMessageConfig, + ToolSearchConfig, UserInputHandler, _capabilities_to_dict, _PermissionHandlerFn, @@ -254,6 +259,16 @@ def _session_limits_to_wire(config: Mapping[str, Any]) -> dict[str, Any]: return wire +def _tool_search_to_wire(config: Mapping[str, Any]) -> dict[str, Any]: + """Convert a ``ToolSearchConfig`` mapping to wire format.""" + wire: dict[str, Any] = {} + if "enabled" in config: + wire["enabled"] = config["enabled"] + if "defer_threshold" in config: + wire["deferThreshold"] = config["defer_threshold"] + return wire + + class TelemetryConfig(TypedDict, total=False): """Configuration for OpenTelemetry integration with the Copilot CLI.""" @@ -348,6 +363,33 @@ def for_uri(url: str, *, connection_token: str | None = None) -> UriRuntimeConne """ return UriRuntimeConnection(url=url, connection_token=connection_token) + @staticmethod + def for_inprocess() -> InProcessRuntimeConnection: + """Host the runtime **in-process** via its native C ABI (FFI). + + **Experimental.** The in-process (FFI) transport is experimental and its + behavior may change or be removed in a future release. + + Instead of spawning the runtime as a child process, the SDK loads the + runtime's native shared library into this process and drives JSON-RPC + over its C ABI. + + Because the runtime loads into this single shared process, per-client + options that lower to environment variables or a working directory + cannot be honored: :attr:`CopilotClientOptions.env`, + :attr:`CopilotClientOptions.telemetry`, and + :attr:`CopilotClientOptions.working_directory` are rejected with this + transport. Set those on the host process before creating the client. + Set ``COPILOT_CLI_PATH`` only when using an externally provisioned + compatible runtime package. + + Note: + Pre-provision the native runtime with + ``python -m copilot download-runtime --in-process`` when automatic + downloads are disabled. + """ + return InProcessRuntimeConnection() + @dataclass class ChildProcessRuntimeConnection(RuntimeConnection): @@ -362,6 +404,13 @@ class ChildProcessRuntimeConnection(RuntimeConnection): args: Sequence[str] = () """Extra command-line arguments passed to the runtime process.""" + env: dict[str, str] | None = None + """Per-connection environment variables for the spawned child process. + + When set, do not also set :attr:`CopilotClientOptions.env` — the client + rejects setting environment in both places. ``None`` inherits the + client-level env (or the current process env).""" + @dataclass class StdioRuntimeConnection(ChildProcessRuntimeConnection): @@ -399,6 +448,19 @@ class UriRuntimeConnection(RuntimeConnection): """Shared secret to authenticate the connection.""" +@dataclass +class InProcessRuntimeConnection(RuntimeConnection): + """Hosts the runtime in-process via its native C ABI (FFI). + + **Experimental.** The in-process (FFI) transport is experimental and its + behavior may change or be removed in a future release. + + Construct via :meth:`RuntimeConnection.for_inprocess`. The runtime's native + shared library is loaded into this process and JSON-RPC is driven over its + C ABI. + """ + + class _GitHubTelemetryAdapter: """Adapts a user-provided ``on_github_telemetry`` callback to the generated ``GitHubTelemetryHandler`` protocol. @@ -419,6 +481,25 @@ async def event(self, params: GitHubTelemetryNotification) -> None: logger.warning("Error handling gitHubTelemetry.event notification", exc_info=True) +class _HooksAdapter: + """Adapts session-scoped hook dispatch to the generated ``HooksHandler`` protocol. + + ``hooks.invoke`` is a client-global RPC method whose payload carries a + ``sessionId``. This adapter routes each invocation to the matching session's + registered hook handlers. + """ + + def __init__(self, get_session: Callable[[str], CopilotSession | None]) -> None: + self._get_session = get_session + + async def invoke(self, params: _HookInvokeRequest) -> _HookInvokeResponse: + session = self._get_session(params.session_id) + if session is None: + raise ValueError(f"unknown session {params.session_id}") + output = await session._handle_hooks_invoke(params.hook_type.value, params.input) + return _HookInvokeResponse(output=output) + + @dataclass class _CopilotClientOptions: """Internal configuration carrier used by :class:`CopilotClient`. @@ -1036,15 +1117,23 @@ def _session_lifecycle_event_from_dict(data: dict) -> SessionLifecycleEvent: _CLI_PROCESS_EXIT_TIMEOUT_SECONDS = 5 -def _get_or_download_cli() -> str | None: +def _get_or_download_cli(*, include_runtime_lib: bool = False) -> str | None: """Get the cached CLI binary, downloading if necessary. Returns the path to the CLI binary, or None if unavailable (dev install with no pinned version, or auto-download disabled). + + When ``include_runtime_lib`` is set, also ensures the native in-process FFI + runtime is available (downloading it on first use). """ from ._cli_download import get_or_download_cli - return get_or_download_cli() + cli_path = get_or_download_cli() + if cli_path and include_runtime_lib: + from ._cli_download import ensure_runtime_library + + ensure_runtime_library(cli_path) + return cli_path def _extract_transform_callbacks( @@ -1082,6 +1171,78 @@ def _extract_transform_callbacks( return wire_payload, callbacks +_DEFAULT_CONNECTION_ENV_VAR = "COPILOT_SDK_DEFAULT_CONNECTION" + + +def _resolve_default_connection(env: Mapping[str, str]) -> RuntimeConnection: + """Resolve the transport when the caller supplies no explicit connection. + + Honors the ``COPILOT_SDK_DEFAULT_CONNECTION`` override (``"inprocess"`` or + ``"stdio"``); defaults to stdio. Matches the Node/.NET/Rust default-transport + override so the CI matrix can run the whole suite under either transport. + """ + value = env.get(_DEFAULT_CONNECTION_ENV_VAR) + if value is None or value == "": + return RuntimeConnection.for_stdio() + normalized = value.strip().lower() + if normalized == "inprocess": + return RuntimeConnection.for_inprocess() + if normalized == "stdio": + return RuntimeConnection.for_stdio() + raise ValueError( + f"Invalid {_DEFAULT_CONNECTION_ENV_VAR}={value!r}. Expected 'inprocess', 'stdio', or unset." + ) + + +def _validate_environment_options( + options: _CopilotClientOptions, connection: RuntimeConnection +) -> None: + """Validate env/telemetry/working-directory options against the transport. + + Per-client environment is only representable for child-process transports + (each client owns its own OS process). The in-process (FFI) transport loads + the native runtime into the shared host process, whose single environment + block and process-global working directory cannot carry per-client values, + so options that lower to them are rejected there (fail loud, not silent). + """ + if isinstance(connection, InProcessRuntimeConnection): + if options.env is not None: + raise ValueError( + "env is not supported with RuntimeConnection.for_inprocess(): the " + "in-process transport loads the native runtime into the shared host " + "process, whose single environment block cannot carry per-client " + "values. Set the variables on the host process environment instead." + ) + if options.telemetry is not None: + raise ValueError( + "telemetry is not supported with RuntimeConnection.for_inprocess(): " + "telemetry configuration is lowered to environment variables read by " + "native runtime code running in the shared host process, so per-client " + "telemetry cannot be honored in-process. Configure telemetry via the " + "host process environment, or use a child-process transport." + ) + if options.working_directory is not None: + raise ValueError( + "working_directory is not supported with RuntimeConnection.for_inprocess(): " + "the native runtime shares the host process working directory, so a " + "per-client working directory cannot be honored in-process. Use a " + "child-process " + "transport, or set the process working directory before creating the client." + ) + return + + if ( + isinstance(connection, ChildProcessRuntimeConnection) + and connection.env is not None + and options.env is not None + ): + raise ValueError( + "Set environment variables via either the client-level env argument or " + "ChildProcessRuntimeConnection.env, not both. Prefer the connection-level " + "env for child-process transports." + ) + + class CopilotClient: """ Main client for interacting with the Copilot CLI. @@ -1139,10 +1300,12 @@ def __init__( """ Initialize a new CopilotClient. - All process-management options (``working_directory``, ``log_level``, - ``env``, ``github_token``, …) apply only when the SDK spawns the runtime - (stdio / tcp connections). They are ignored when connecting to an - existing runtime via :meth:`RuntimeConnection.for_uri`. + Runtime options apply to locally hosted connections. The in-process + transport supports typed runtime options such as ``log_level``, + ``github_token``, and ``base_directory``, but rejects per-client + ``working_directory``, ``env``, and ``telemetry``. Options are ignored + when connecting to an existing runtime via + :meth:`RuntimeConnection.for_uri`. Args: connection: How to reach the runtime. Defaults to @@ -1216,8 +1379,11 @@ def __init__( mode=mode, ) connection = ( - options.connection if options.connection is not None else RuntimeConnection.for_stdio() + options.connection + if options.connection is not None + else _resolve_default_connection(os.environ) ) + _validate_environment_options(options, connection) _require_storage_for_empty_mode( mode=options.mode, base_directory=options.base_directory, @@ -1233,6 +1399,9 @@ def __init__( # Resolve connection-mode-specific state. self._actual_host: str = "localhost" self._is_external_server: bool = isinstance(connection, UriRuntimeConnection) + self._cli_path_source: str | None = None + self._ffi_host: FfiRuntimeHost | None = None + self._inprocess_runtime_path: str | None = None if isinstance(connection, UriRuntimeConnection): if connection.connection_token is not None and len(connection.connection_token) == 0: @@ -1240,6 +1409,15 @@ def __init__( self._actual_host, actual_port = self._parse_cli_url(connection.url) self._runtime_port: int | None = actual_port self._effective_connection_token: str | None = connection.connection_token + elif isinstance(connection, InProcessRuntimeConnection): + # In-process (FFI): no child process and no per-connection token. + self._runtime_port = None + self._effective_connection_token = None + self._inprocess_runtime_path = self._resolve_runtime_entrypoint( + None, include_runtime_lib=True + ) + if options.use_logged_in_user is None: + options.use_logged_in_user = not bool(options.github_token) else: assert isinstance(connection, ChildProcessRuntimeConnection) self._runtime_port = None @@ -1259,26 +1437,17 @@ def __init__( self._effective_connection_token = None # Resolve CLI path: explicit > COPILOT_CLI_PATH env var > downloaded binary. - effective_env = options.env if options.env is not None else os.environ - self._cli_path_source: str | None = "explicit" - if connection.path is None: - env_cli_path = effective_env.get("COPILOT_CLI_PATH") - if env_cli_path: - connection.path = env_cli_path - self._cli_path_source = "environment" - else: - downloaded_path = _get_or_download_cli() - if downloaded_path: - connection.path = downloaded_path - self._cli_path_source = "downloaded" - else: - raise RuntimeError( - "Copilot CLI not found. Install a published wheel (which " - "auto-downloads the CLI on first use), set COPILOT_CLI_PATH, " - "or pass an explicit path via " - "RuntimeConnection.for_stdio(path=...) / " - "RuntimeConnection.for_tcp(path=...)." - ) + # Select the environment by identity, not truthiness, so an intentionally + # empty per-connection or client env stays authoritative (the spawned child + # receives that empty mapping) instead of falling back to os.environ and + # unexpectedly honoring a host COPILOT_CLI_PATH. + if connection.env is not None: + effective_env: Mapping[str, str] = connection.env + elif options.env is not None: + effective_env = options.env + else: + effective_env = os.environ + connection.path = self._resolve_runtime_entrypoint(connection.path, env=effective_env) # Resolve use_logged_in_user default if options.use_logged_in_user is None: @@ -1304,6 +1473,58 @@ def __init__( self._session_fs_config = options.session_fs self._request_handler = options.request_handler + def _resolve_runtime_entrypoint( + self, + path: str | None, + *, + env: Mapping[str, str] | None = None, + include_runtime_lib: bool = False, + ) -> str: + """Resolve the runtime executable path (explicit > env > downloaded). + + Sets ``self._cli_path_source`` for diagnostics. When + ``include_runtime_lib`` is set (in-process transport), also ensures the + native runtime library is downloaded alongside the CLI. + + Raises: + RuntimeError: If no runtime path can be resolved. + """ + if path is not None: + self._cli_path_source = "explicit" + return self._ensure_runtime_lib(path) if include_runtime_lib else path + + lookup = env if env is not None else os.environ + env_cli_path = lookup.get("COPILOT_CLI_PATH") + if env_cli_path: + self._cli_path_source = "environment" + return self._ensure_runtime_lib(env_cli_path) if include_runtime_lib else env_cli_path + + downloaded_path = _get_or_download_cli(include_runtime_lib=include_runtime_lib) + if downloaded_path: + self._cli_path_source = "downloaded" + return downloaded_path + + raise RuntimeError( + "Copilot CLI not found. Install a published wheel (which " + "auto-downloads the CLI on first use), set COPILOT_CLI_PATH, " + "or pass an explicit path via " + "RuntimeConnection.for_stdio(path=...) / " + "RuntimeConnection.for_tcp(path=...)." + ) + + @staticmethod + def _ensure_runtime_lib(cli_path: str) -> str: + """Ensure the in-process runtime library sits next to a user-supplied CLI. + + For explicit/``COPILOT_CLI_PATH`` entrypoints, the native library may + already be bundled (dev ``prebuilds`` layout); otherwise it is fetched on + first use. Returns ``cli_path`` unchanged. + """ + from ._cli_download import ensure_runtime_library + + ensure_runtime_library(cli_path) + return cli_path + @property def rpc(self) -> ServerRpc: """Typed server-scoped RPC methods.""" @@ -1542,7 +1763,11 @@ async def stop(self) -> None: StopError(message=f"Failed to disconnect session {session.session_id}: {e}") ) - if self._rpc is not None and self._cli_process is not None and not self._is_external_server: + if ( + self._rpc is not None + and (self._cli_process is not None or self._ffi_host is not None) + and not self._is_external_server + ): runtime_shutdown_start = time.perf_counter() try: await self._rpc.runtime.shutdown(timeout=_RUNTIME_SHUTDOWN_TIMEOUT_SECONDS) @@ -1572,6 +1797,15 @@ async def stop(self) -> None: async with self._models_cache_lock: self._models_cache = None + # Dispose the in-process FFI host and release the loaded native library. + if self._ffi_host is not None: + try: + self._ffi_host.dispose() + except Exception: + logger.debug("Error while disposing in-process FFI host", exc_info=True) + self._ffi_host = None + self._process = None + # Close TCP socket wrappers without treating them as owned processes. if self._process is not None and self._process is not self._cli_process: try: @@ -1665,6 +1899,15 @@ async def force_stop(self) -> None: except Exception: logger.debug("Error while force-stopping Copilot CLI process", exc_info=True) + # Force-dispose the in-process FFI host before tearing down JSON-RPC. + if self._ffi_host is not None: + try: + self._ffi_host.dispose() + except Exception: + logger.debug("Error while force-disposing in-process FFI host", exc_info=True) + self._ffi_host = None + self._process = None + # Then clean up the JSON-RPC client if self._client: try: @@ -1696,6 +1939,7 @@ async def create_session( context_tier: ContextTier | None = None, tools: list[Tool] | None = None, system_message: SystemMessageConfig | None = None, + tool_search: ToolSearchConfig | None = None, available_tools: list[str] | ToolSet | None = None, excluded_tools: list[str] | ToolSet | None = None, on_user_input_request: UserInputHandler | None = None, @@ -1754,8 +1998,10 @@ async def create_session( request_extensions: bool | None = None, extension_sdk_path: str | None = None, extension_info: ExtensionInfo | None = None, + canvas_provider: CanvasProviderIdentity | None = None, canvas_handler: CanvasHandler | None = None, exp_assignments: dict[str, Any] | None = None, + enable_managed_settings: bool | None = None, ) -> CopilotSession: """ Create a new conversation session with the Copilot CLI. @@ -1887,6 +2133,13 @@ async def create_session( malformed payloads are dropped by the runtime (fail-open). This is an internal/trusted-integrator option. Sent on the wire as ``expAssignments``. + enable_managed_settings: Opt-in flag. When ``True``, the runtime + self-fetches enterprise managed settings (bypass-permissions + policy) at session bootstrap using the session's ``github_token``. + Requires ``github_token`` to be set; if omitted, the runtime is + expected to reject session creation (fail-closed). When unset, + behaves exactly as before. Sent on the wire as + ``enableManagedSettings``. Returns: A :class:`CopilotSession` instance for the new session. @@ -1926,6 +2179,8 @@ async def create_session( definition["skipPermission"] = True if tool.defer is not None: definition["defer"] = tool.defer + if tool.metadata is not None: + definition["metadata"] = tool.metadata tool_defs.append(definition) # Empty-mode validation and normalization @@ -1970,6 +2225,9 @@ async def create_session( if wire_system_message: payload["systemMessage"] = wire_system_message + if tool_search is not None: + payload["toolSearch"] = _tool_search_to_wire(tool_search) + if available_tools is not None: payload["availableTools"] = available_tools if excluded_tools is not None: @@ -2018,6 +2276,10 @@ async def create_session( if exp_assignments is not None: payload["expAssignments"] = exp_assignments + # Opt the runtime into self-fetching enterprise managed settings + if enable_managed_settings is not None: + payload["enableManagedSettings"] = enable_managed_settings + # Add working directory if provided if working_directory: payload["workingDirectory"] = working_directory @@ -2160,6 +2422,8 @@ async def create_session( payload["extensionSdkPath"] = extension_sdk_path if extension_info is not None: payload["extensionInfo"] = extension_info.to_dict() + if canvas_provider is not None: + payload["canvasProvider"] = canvas_provider.to_dict() if not self._client: raise RuntimeError("Client not connected") @@ -2347,6 +2611,7 @@ async def resume_session( context_tier: ContextTier | None = None, tools: list[Tool] | None = None, system_message: SystemMessageConfig | None = None, + tool_search: ToolSearchConfig | None = None, available_tools: list[str] | ToolSet | None = None, excluded_tools: list[str] | ToolSet | None = None, on_user_input_request: UserInputHandler | None = None, @@ -2405,9 +2670,11 @@ async def resume_session( request_extensions: bool | None = None, extension_sdk_path: str | None = None, extension_info: ExtensionInfo | None = None, + canvas_provider: CanvasProviderIdentity | None = None, canvas_handler: CanvasHandler | None = None, open_canvases: list[OpenCanvasInstance] | None = None, exp_assignments: dict[str, Any] | None = None, + enable_managed_settings: bool | None = None, ) -> CopilotSession: """ Resume an existing conversation session by its ID. @@ -2540,6 +2807,13 @@ async def resume_session( malformed payloads are dropped by the runtime (fail-open). This is an internal/trusted-integrator option. Sent on the wire as ``expAssignments``. + enable_managed_settings: Opt-in flag. When ``True``, the runtime + self-fetches enterprise managed settings (bypass-permissions + policy) at session bootstrap using the session's ``github_token``. + Requires ``github_token`` to be set; if omitted, the runtime is + expected to reject session creation (fail-closed). When unset, + behaves exactly as before. Sent on the wire as + ``enableManagedSettings``. Returns: A :class:`CopilotSession` instance for the resumed session. @@ -2581,6 +2855,8 @@ async def resume_session( definition["skipPermission"] = True if tool.defer is not None: definition["defer"] = tool.defer + if tool.metadata is not None: + definition["metadata"] = tool.metadata tool_defs.append(definition) # Empty-mode validation and normalization @@ -2621,6 +2897,8 @@ async def resume_session( wire_system_message, transform_callbacks = _extract_transform_callbacks(system_message) if wire_system_message: payload["systemMessage"] = wire_system_message + if tool_search is not None: + payload["toolSearch"] = _tool_search_to_wire(tool_search) if available_tools is not None: payload["availableTools"] = available_tools if excluded_tools is not None: @@ -2695,6 +2973,10 @@ async def resume_session( if exp_assignments is not None: payload["expAssignments"] = exp_assignments + # Opt the runtime into self-fetching enterprise managed settings + if enable_managed_settings is not None: + payload["enableManagedSettings"] = enable_managed_settings + if working_directory: payload["workingDirectory"] = working_directory if config_directory: @@ -2783,6 +3065,8 @@ async def resume_session( payload["extensionSdkPath"] = extension_sdk_path if extension_info is not None: payload["extensionInfo"] = extension_info.to_dict() + if canvas_provider is not None: + payload["canvasProvider"] = canvas_provider.to_dict() if not self._client: raise RuntimeError("Client not connected") @@ -3477,6 +3761,8 @@ def _convert_custom_agent_to_wire_format( wire_agent["skills"] = agent["skills"] if "model" in agent: wire_agent["model"] = agent["model"] + if "reasoning_effort" in agent: + wire_agent["reasoningEffort"] = agent["reasoning_effort"] return wire_agent def _convert_default_agent_to_wire_format( @@ -3500,11 +3786,15 @@ async def _start_cli_server(self) -> None: """Start the runtime process. This spawns the runtime as a subprocess using the configured transport - mode (stdio or TCP). + mode (stdio or TCP), or hosts it in-process for the FFI transport. Raises: RuntimeError: If the server fails to start or times out. """ + if isinstance(self._connection, InProcessRuntimeConnection): + await self._start_inprocess_ffi() + return + assert isinstance(self._connection, ChildProcessRuntimeConnection) conn = self._connection opts = self._options @@ -3557,8 +3847,13 @@ async def _start_cli_server(self) -> None: }, ) - # Get environment variables - if opts.env is None: + # Get environment variables. Per-connection env (ChildProcessRuntimeConnection.env) + # takes precedence over the client-level env; the constructor already rejects + # setting both. When neither is set, inherit the current process environment. + conn_env = conn.env if isinstance(conn, ChildProcessRuntimeConnection) else None + if conn_env is not None: + env = dict(conn_env) + elif opts.env is None: env = dict(os.environ) else: env = dict(opts.env) @@ -3673,6 +3968,69 @@ async def read_port(): except TimeoutError: raise RuntimeError("Timeout waiting for CLI server to start") + async def _start_inprocess_ffi(self) -> None: + """Host the runtime in-process via the native FFI library. + + Loads the native runtime library and opens the FFI JSON-RPC connection. + + Raises: + RuntimeError: If the native library is missing or startup fails. + """ + assert isinstance(self._connection, InProcessRuntimeConnection) + runtime_path = self._inprocess_runtime_path + assert runtime_path is not None # resolved in __init__ + + logger.info( + "CopilotClient._start_inprocess_ffi hosting Copilot runtime in-process", + extra={"runtime_path": runtime_path, "runtime_path_source": self._cli_path_source}, + ) + + opts = self._options + args: list[str] = [] + if opts.log_level: + args.extend(["--log-level", opts.log_level]) + if opts.github_token: + args.extend(["--auth-token-env", "COPILOT_SDK_AUTH_TOKEN"]) + if not opts.use_logged_in_user: + args.append("--no-auto-login") + if opts.session_idle_timeout_seconds is not None and opts.session_idle_timeout_seconds > 0: + args.extend(["--session-idle-timeout", str(opts.session_idle_timeout_seconds)]) + if opts.enable_remote_sessions: + args.append("--remote") + + environment: dict[str, str] = {} + if opts.github_token: + environment["COPILOT_SDK_AUTH_TOKEN"] = opts.github_token + if opts.base_directory: + environment["COPILOT_HOME"] = opts.base_directory + if opts.mode == "empty": + environment["COPILOT_DISABLE_KEYTAR"] = "1" + + host = FfiRuntimeHost.create( + runtime_path, + environment=environment or None, + args=tuple(args), + ) + + # Track the host and expose its process-like adapter *before* the blocking + # handshake. asyncio.to_thread keeps running host_start after a cancellation + # (a thread can't be interrupted), and CancelledError bypasses start()'s + # `except Exception`, so assigning here — as .NET does before StartAsync — + # keeps a completed native host owned so stop()/force_stop() can dispose it + # instead of leaking it. + self._ffi_host = host + self._process = host.process + + ffi_start = time.perf_counter() + # Native startup may block, so run the handshake off the event loop. + await asyncio.to_thread(host.start_blocking) + log_timing( + logger, + logging.DEBUG, + "CopilotClient._start_inprocess_ffi FFI host started", + ffi_start, + ) + async def _connect_to_server(self) -> None: """Connect to the runtime via the configured transport. @@ -3682,7 +4040,9 @@ async def _connect_to_server(self) -> None: RuntimeError: If the connection fails. """ setup_start = time.perf_counter() - if isinstance(self._connection, StdioRuntimeConnection): + if isinstance(self._connection, (StdioRuntimeConnection, InProcessRuntimeConnection)): + # The in-process FFI host exposes a process-like adapter (stdin/stdout), + # so the same stdio JSON-RPC wiring drives it unchanged. await self._connect_via_stdio() else: await self._connect_via_tcp() @@ -3735,7 +4095,6 @@ def handle_notification(method: str, params: dict): self._client.set_request_handler( "autoModeSwitch.request", self._handle_auto_mode_switch_request ) - self._client.set_request_handler("hooks.invoke", self._handle_hooks_invoke) self._client.set_request_handler( "systemMessage.transform", self._handle_system_message_transform ) @@ -3855,7 +4214,6 @@ def handle_notification(method: str, params: dict): self._client.set_request_handler( "autoModeSwitch.request", self._handle_auto_mode_switch_request ) - self._client.set_request_handler("hooks.invoke", self._handle_hooks_invoke) self._client.set_request_handler( "systemMessage.transform", self._handle_system_message_transform ) @@ -3945,16 +4303,19 @@ def _register_client_global_handlers(self) -> None: github_telemetry_adapter = None if self._on_github_telemetry is not None: github_telemetry_adapter = _GitHubTelemetryAdapter(self._on_github_telemetry) - if llm_inference_adapter is None and github_telemetry_adapter is None: - return register_client_global_api_handlers( self._client, ClientGlobalApiHandlers( + hooks=_HooksAdapter(self._get_session), llm_inference=llm_inference_adapter, git_hub_telemetry=github_telemetry_adapter, ), ) + def _get_session(self, session_id: str) -> CopilotSession | None: + with self._sessions_lock: + return self._sessions.get(session_id) + async def _set_llm_inference_provider(self) -> None: if self._request_handler is None or self._rpc is None: return @@ -4027,34 +4388,6 @@ async def _handle_auto_mode_switch_request(self, params: dict) -> dict: response = await session._handle_auto_mode_switch_request(params) return {"response": response} - async def _handle_hooks_invoke(self, params: dict) -> dict: - """ - Handle a hooks invocation from the CLI server. - - Args: - params: The hooks invocation parameters from the server. - - Returns: - A dict containing the hook output. - - Raises: - ValueError: If the request payload is invalid. - """ - session_id = params.get("sessionId") - hook_type = params.get("hookType") - input_data = params.get("input") - - if not session_id or not hook_type: - raise ValueError("invalid hooks invoke payload") - - with self._sessions_lock: - session = self._sessions.get(session_id) - if not session: - raise ValueError(f"unknown session {session_id}") - - output = await session._handle_hooks_invoke(hook_type, input_data) - return {"output": output} - async def _handle_system_message_transform(self, params: dict) -> dict: """Handle a systemMessage.transform request from the CLI server.""" session_id = params.get("sessionId") diff --git a/python/copilot/copilot_request_handler.py b/python/copilot/copilot_request_handler.py index 54e71027c9..e6465b7bbc 100644 --- a/python/copilot/copilot_request_handler.py +++ b/python/copilot/copilot_request_handler.py @@ -96,6 +96,15 @@ class CopilotRequestContext: """Id of the runtime session that triggered this request, when in scope. Absent for out-of-session requests (e.g. the startup model catalog).""" + agent_id: str | None = None + """Stable per-agent-instance id for the agent trajectory that issued this request.""" + + parent_agent_id: str | None = None + """Id of the parent agent when this request was issued by a subagent.""" + + interaction_type: str | None = None + """Runtime classification for the interaction that produced this request.""" + _bridge: _CopilotWebSocketResponseBridge | None = field(default=None, repr=False) @@ -253,6 +262,9 @@ async def _dispatch(self, exchange: _CopilotRequestExchange) -> None: ctx = CopilotRequestContext( request_id=exchange.request_id, session_id=exchange.session_id, + agent_id=exchange.agent_id, + parent_agent_id=exchange.parent_agent_id, + interaction_type=exchange.interaction_type, transport=exchange.transport, url=exchange.url, headers=exchange.headers, @@ -382,6 +394,9 @@ def __init__( ) -> None: self.request_id = request_id self.session_id: str | None = None + self.agent_id: str | None = None + self.parent_agent_id: str | None = None + self.interaction_type: str | None = None self.method: str = "GET" self.url: str = "" self.headers: dict[str, list[str]] = {} @@ -397,6 +412,9 @@ def __init__( def set_context(self, params: LlmInferenceHTTPRequestStartRequest) -> None: """Fill in the request context once the matching start frame arrives.""" self.session_id = params.session_id + self.agent_id = params.agent_id + self.parent_agent_id = params.parent_agent_id + self.interaction_type = params.interaction_type self.method = params.method self.url = params.url self.headers = params.headers diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index c3a9897c86..828eaa3ce4 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, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpServerSource, McpServerStatus, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, UserToolSessionApproval +from .session_events import AbortReason, Attachment, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpServerSource, McpServerStatus, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, UserToolSessionApproval, Verbosity if TYPE_CHECKING: from .._jsonrpc import JsonRpcClient @@ -793,65 +793,6 @@ def to_dict(self) -> dict: result["instanceId"] = from_str(self.instance_id) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class OpenCanvasInstance: - """Open canvas instance snapshot.""" - - canvas_id: str - """Provider-local canvas identifier""" - - extension_id: str - """Owning provider identifier""" - - instance_id: str - """Stable caller-supplied canvas instance identifier""" - - extension_name: str | None = None - """Owning extension display name, when available""" - - input: Any = None - """Input supplied when the instance was opened""" - - status: str | None = None - """Provider-supplied status text""" - - title: str | None = None - """Rendered title""" - - url: str | None = None - """URL for web-rendered canvases""" - - @staticmethod - def from_dict(obj: Any) -> 'OpenCanvasInstance': - assert isinstance(obj, dict) - canvas_id = from_str(obj.get("canvasId")) - extension_id = from_str(obj.get("extensionId")) - instance_id = from_str(obj.get("instanceId")) - extension_name = from_union([from_str, from_none], obj.get("extensionName")) - input = obj.get("input") - status = from_union([from_str, from_none], obj.get("status")) - title = from_union([from_str, from_none], obj.get("title")) - url = from_union([from_str, from_none], obj.get("url")) - return OpenCanvasInstance(canvas_id, extension_id, instance_id, extension_name, input, status, title, url) - - def to_dict(self) -> dict: - result: dict = {} - result["canvasId"] = from_str(self.canvas_id) - result["extensionId"] = from_str(self.extension_id) - result["instanceId"] = from_str(self.instance_id) - if self.extension_name is not None: - result["extensionName"] = from_union([from_str, from_none], self.extension_name) - if self.input is not None: - result["input"] = self.input - if self.status is not None: - result["status"] = from_union([from_str, from_none], self.status) - if self.title is not None: - result["title"] = from_union([from_str, from_none], self.title) - if self.url is not None: - result["url"] = from_union([from_str, from_none], self.url) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class CanvasOpenRequest: @@ -2505,6 +2446,46 @@ 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" + +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _HookInvokeResponse: + """Optional output returned by an SDK callback hook.""" + + output: Any = None + + @staticmethod + def from_dict(obj: Any) -> '_HookInvokeResponse': + assert isinstance(obj, dict) + output = obj.get("output") + return _HookInvokeResponse(output) + + def to_dict(self) -> dict: + result: dict = {} + if self.output is not None: + result["output"] = self.output + return result + class PurpleSource(Enum): GITHUB = "github" LOCAL = "local" @@ -3645,6 +3626,13 @@ def to_dict(self) -> dict: result["serverName"] = from_str(self.server_name) return result +# Experimental: this type is part of an experimental API and may change or be removed. +class MCPToolUIVisibility(Enum): + """Consumer allowed to call an MCP tool.""" + + APP = "app" + MODEL = "model" + class MCPOauthPendingRequestResponseKind(Enum): CANCELLED = "cancelled" TOKEN = "token" @@ -3741,6 +3729,124 @@ def to_dict(self) -> dict: result["removed"] = from_bool(self.removed) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPResourceContent: + """MCP resource content with URI, optional MIME type, text or base64 blob, and resource + metadata. + """ + uri: str + """The resource URI""" + + meta: dict[str, Any] | None = None + """Resource-level metadata (CSP, permissions, etc.)""" + + blob: str | None = None + """Base64-encoded binary content""" + + mime_type: str | None = None + """MIME type of the content""" + + text: str | None = None + """Text content (e.g. HTML)""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPResourceContent': + assert isinstance(obj, dict) + uri = from_str(obj.get("uri")) + meta = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("_meta")) + blob = from_union([from_str, from_none], obj.get("blob")) + mime_type = from_union([from_str, from_none], obj.get("mimeType")) + text = from_union([from_str, from_none], obj.get("text")) + return MCPResourceContent(uri, meta, blob, mime_type, text) + + def to_dict(self) -> dict: + result: dict = {} + result["uri"] = from_str(self.uri) + if self.meta is not None: + result["_meta"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.meta) + if self.blob is not None: + result["blob"] = from_union([from_str, from_none], self.blob) + if self.mime_type is not None: + result["mimeType"] = from_union([from_str, from_none], self.mime_type) + if self.text is not None: + result["text"] = from_union([from_str, from_none], self.text) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPResourcesListRequest: + """MCP server whose resources to enumerate.""" + + server_name: str + """Name of the MCP server whose resources to enumerate""" + + cursor: str | None = None + """Opaque MCP pagination cursor from a prior `nextCursor` value""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPResourcesListRequest': + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + cursor = from_union([from_str, from_none], obj.get("cursor")) + return MCPResourcesListRequest(server_name, cursor) + + def to_dict(self) -> dict: + result: dict = {} + result["serverName"] = from_str(self.server_name) + if self.cursor is not None: + result["cursor"] = from_union([from_str, from_none], self.cursor) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPResourcesListTemplatesRequest: + """MCP server whose resource templates to enumerate.""" + + server_name: str + """Name of the MCP server whose resource templates to enumerate""" + + cursor: str | None = None + """Opaque MCP pagination cursor from a prior `nextCursor` value""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPResourcesListTemplatesRequest': + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + cursor = from_union([from_str, from_none], obj.get("cursor")) + return MCPResourcesListTemplatesRequest(server_name, cursor) + + def to_dict(self) -> dict: + result: dict = {} + result["serverName"] = from_str(self.server_name) + if self.cursor is not None: + result["cursor"] = from_union([from_str, from_none], self.cursor) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPResourcesReadRequest: + """MCP server and resource URI to fetch.""" + + server_name: str + """Name of the MCP server hosting the resource""" + + uri: str + """Resource URI""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPResourcesReadRequest': + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + uri = from_str(obj.get("uri")) + return MCPResourcesReadRequest(server_name, uri) + + def to_dict(self) -> dict: + result: dict = {} + result["serverName"] = from_str(self.server_name) + result["uri"] = from_str(self.uri) + return result + # Experimental: this type is part of an experimental API and may change or be removed. class MCPSamplingExecutionAction(Enum): """Outcome of the sampling inference. 'success' produced a response; 'failure' encountered @@ -4061,8 +4167,11 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MetadataSetWorkingDirectoryRequest: - """Absolute path to set as the session's new working directory.""" - + """Absolute path to set as the session's new working directory. For local sessions the path + must be absolute and exist on disk: it is validated before any session state changes, and + a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote + sessions record the path as-is. + """ working_directory: str """Absolute path to set as the session's working directory. The runtime updates the session's recorded cwd so subsequent operations (shell tools, file lookups, telemetry) @@ -4084,9 +4193,13 @@ def to_dict(self) -> dict: @dataclass class MetadataSetWorkingDirectoryResult: """Update the session's working directory. Used by the host when the user explicitly changes - cwd (e.g., the `/cd` slash command). The host is responsible for `process.chdir` and any - related side-effects (file index, etc.); this method only updates the session's own - recorded path. + cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects + (file index, etc.); it does NOT change the process working directory (a session's cwd is + per-session, not process-global). For local sessions the runtime validates the target + first (an absolute path that exists on disk) and re-bases the permission primary + directory; a rejected validation fails the call before anything is mutated, persisted, or + emitted. Location-scoped permission rules are then re-keyed to the new directory + (best-effort). Remote sessions only record the path. """ working_directory: str """Working directory after the update""" @@ -4171,6 +4284,50 @@ def to_dict(self) -> dict: result["mode"] = to_enum(SessionMode, self.mode) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelBillingPromo: + """Active server-driven promotion for this model, if any. Present when the model is being + promoted with a time-boxed discount. + + Active server-driven promotion for a model, including its discount and expiry. + """ + ends_at: str + """UTC ISO 8601 timestamp marking when the promotion ends. Always present: the API only + surfaces a promo whose expiry parses and is in the future. Consumers should treat a past + value as expired. + """ + discount_percent: float | None = None + """Percentage discount (0-100) applied while the promotion is active. May be fractional.""" + + id: str | None = None + """Stable identifier for the promotion campaign.""" + + message: str | None = None + """Human-readable promotion message. Does not include the expiry timestamp; consumers may + format endsAt and append it. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelBillingPromo': + assert isinstance(obj, dict) + ends_at = from_str(obj.get("endsAt")) + discount_percent = from_union([from_float, from_none], obj.get("discountPercent")) + id = from_union([from_str, from_none], obj.get("id")) + message = from_union([from_str, from_none], obj.get("message")) + return ModelBillingPromo(ends_at, discount_percent, id, message) + + def to_dict(self) -> dict: + result: dict = {} + result["endsAt"] = from_str(self.ends_at) + if self.discount_percent is not None: + result["discountPercent"] = from_union([to_float, from_none], self.discount_percent) + if self.id is not None: + 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) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ModelBillingTokenPricesLongContext: @@ -5590,7 +5747,7 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PluginsMarketplacesAddRequest: - """Marketplace source to register.""" + """Marketplace source and optional working directory for relative-path resolution.""" source: str """Marketplace source. Accepts the same forms as the CLI: "owner/repo" or "owner/repo#ref" @@ -5598,16 +5755,23 @@ class PluginsMarketplacesAddRequest: (user@host:path), or a local path. The marketplace's own name (from its manifest) is used as the registration key. """ + working_directory: str | None = None + """Working directory used to resolve relative local paths in `source`. Defaults to the + server's current working directory. + """ @staticmethod def from_dict(obj: Any) -> 'PluginsMarketplacesAddRequest': assert isinstance(obj, dict) source = from_str(obj.get("source")) - return PluginsMarketplacesAddRequest(source) + working_directory = from_union([from_str, from_none], obj.get("workingDirectory")) + return PluginsMarketplacesAddRequest(source, working_directory) def to_dict(self) -> dict: result: dict = {} result["source"] = from_str(self.source) + if self.working_directory is not None: + result["workingDirectory"] = from_union([from_str, from_none], self.working_directory) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -5687,6 +5851,11 @@ class PluginsReloadRequest: reload_custom_agents: bool | None = None """Re-run custom-agent discovery after refreshing plugins. Defaults to true.""" + reload_extensions: bool | None = None + """Re-discover and relaunch subprocess extensions (including plugin-shipped extensions) + after refreshing plugins. Defaults to true. Has no effect when the session has no active + extension controller (e.g. extensions were not requested for the session). + """ reload_hooks: bool | None = None """Re-load user, plugin, and (subject to `deferRepoHooks`) repo hooks. Defaults to true. Has no effect when the host has not registered a hook reloader (e.g. remote sessions). @@ -5699,9 +5868,10 @@ def from_dict(obj: Any) -> 'PluginsReloadRequest': assert isinstance(obj, dict) defer_repo_hooks = from_union([from_bool, from_none], obj.get("deferRepoHooks")) reload_custom_agents = from_union([from_bool, from_none], obj.get("reloadCustomAgents")) + reload_extensions = from_union([from_bool, from_none], obj.get("reloadExtensions")) reload_hooks = from_union([from_bool, from_none], obj.get("reloadHooks")) reload_mcp = from_union([from_bool, from_none], obj.get("reloadMcp")) - return PluginsReloadRequest(defer_repo_hooks, reload_custom_agents, reload_hooks, reload_mcp) + return PluginsReloadRequest(defer_repo_hooks, reload_custom_agents, reload_extensions, reload_hooks, reload_mcp) def to_dict(self) -> dict: result: dict = {} @@ -5709,6 +5879,8 @@ def to_dict(self) -> dict: result["deferRepoHooks"] = from_union([from_bool, from_none], self.defer_repo_hooks) if self.reload_custom_agents is not None: result["reloadCustomAgents"] = from_union([from_bool, from_none], self.reload_custom_agents) + if self.reload_extensions is not None: + result["reloadExtensions"] = from_union([from_bool, from_none], self.reload_extensions) if self.reload_hooks is not None: result["reloadHooks"] = from_union([from_bool, from_none], self.reload_hooks) if self.reload_mcp is not None: @@ -6062,14 +6234,16 @@ class RegisterEventInterestParams: """The event type the consumer wants the runtime to treat as 'observed' for behavior-switching gating. Some runtime code paths inspect whether any consumer is interested in a specific event type and choose a different implementation accordingly - (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates the full - interactive OAuth flow to the consumer; when no interest is registered the runtime - installs a browserless fallback that silently reuses cached tokens). SDK clients that - long-poll events do NOT automatically appear as listeners to these gating checks — they - must explicitly call `registerInterest` for each event type they want the runtime to - count as having a consumer. Multiple registrations for the same event type from the same - or different consumers are tracked independently and must each be released. See: - `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, + (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates interactive + OAuth token acquisition to the consumer via `mcp.oauth_required` events; when no interest + is registered the runtime still attempts non-interactive reconnect from cached or + refreshable tokens, and only marks the server `needs-auth` if usable credentials are + unavailable — it does not open a browser or start interactive OAuth without a consumer). + SDK clients that long-poll events do NOT automatically appear as listeners to these + gating checks — they must explicitly call `registerInterest` for each event type they + want the runtime to count as having a consumer. Multiple registrations for the same event + type from the same or different consumers are tracked independently and must each be + released. See: `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, `session_limits_exhausted.requested`, `user_input.requested`, `elicitation.requested`, `command.queued`, `exit_plan_mode.requested`. """ @@ -6638,6 +6812,9 @@ def to_dict(self) -> dict: class SendAgentMode(Enum): """The UI mode the agent was in when this message was sent. Defaults to the session's current mode. + + The UI mode the agent was in when these messages were sent. Defaults to the session's + current mode. """ AUTOPILOT = "autopilot" INTERACTIVE = "interactive" @@ -6674,14 +6851,96 @@ def to_dict(self) -> dict: result["instanceId"] = from_union([from_str, from_none], self.instance_id) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SendMessageItem: + """A single user message to append to the session as part of a `session.sendMessages` turn""" + + prompt: str + """The user message text""" + + attachments: list[Attachment] | None = None + """Optional attachments (files, directories, selections, blobs, GitHub references) to + include with this message + """ + # Internal: this field is an internal SDK API and is not part of the public surface. + billable: bool | None = None + """If false, this message will not trigger a Premium Request Unit charge. User messages + default to billable. + """ + display_prompt: str | None = None + """If provided, this is shown in the timeline instead of `prompt`""" + + required_tool: str | None = None + """If set, the request will fail if the named tool is not available when this message is + among the user messages at the start of the current exchange + """ + # Internal: this field is an internal SDK API and is not part of the public surface. + source: str | None = None + """Optional provenance tag copied to the resulting user.message event. Must match one of + three forms: the literal `system`, `command-` for messages originating from a + command (e.g. slash command, Mission Control command), or `schedule-` for + messages originating from a scheduled job. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SendMessageItem': + assert isinstance(obj, dict) + prompt = from_str(obj.get("prompt")) + attachments = from_union([lambda x: from_list(Attachment.from_dict, x), from_none], obj.get("attachments")) + billable = from_union([from_bool, from_none], obj.get("billable")) + display_prompt = from_union([from_str, from_none], obj.get("displayPrompt")) + required_tool = from_union([from_str, from_none], obj.get("requiredTool")) + source = from_union([from_str, from_none], obj.get("source")) + return SendMessageItem(prompt, attachments, billable, display_prompt, required_tool, source) + + def to_dict(self) -> dict: + result: dict = {} + result["prompt"] = from_str(self.prompt) + if self.attachments is not None: + result["attachments"] = from_union([lambda x: from_list(lambda x: to_class(Attachment, x), x), from_none], self.attachments) + if self.billable is not None: + result["billable"] = from_union([from_bool, from_none], self.billable) + if self.display_prompt is not None: + result["displayPrompt"] = from_union([from_str, from_none], self.display_prompt) + if self.required_tool is not None: + result["requiredTool"] = from_union([from_str, from_none], self.required_tool) + 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. class SendMode(Enum): - """How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` + """How to deliver the messages. `enqueue` (default) appends to the message queue. + `immediate` interjects during an in-progress turn. + + How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. """ ENQUEUE = "enqueue" IMMEDIATE = "immediate" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SendMessagesResult: + """Result of sending zero or more user messages""" + + message_ids: list[str] + """Unique identifiers assigned to the messages, one per provided message in order. Empty + when no messages were provided. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SendMessagesResult': + assert isinstance(obj, dict) + message_ids = from_list(from_str, obj.get("messageIds")) + return SendMessagesResult(message_ids) + + def to_dict(self) -> dict: + result: dict = {} + result["messageIds"] = from_list(from_str, self.message_ids) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SendResult: @@ -7323,33 +7582,6 @@ def to_dict(self) -> dict: result["startupPrompts"] = from_list(from_str, self.startup_prompts) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SessionModelList: - """The list of models available to this session.""" - - list: list[Any] - """Available models, ordered with the most preferred default first. Includes both Copilot - (CAPI) models and any registry BYOK models; a BYOK model appears under its - provider-qualified selection id (`provider/id`). - """ - quota_snapshots: dict[str, Any] | None = None - """Per-quota snapshots returned alongside the model list, keyed by quota type.""" - - @staticmethod - def from_dict(obj: Any) -> 'SessionModelList': - assert isinstance(obj, dict) - list = from_list(lambda x: x, obj.get("list")) - quota_snapshots = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("quotaSnapshots")) - return SessionModelList(list, quota_snapshots) - - def to_dict(self) -> dict: - result: dict = {} - result["list"] = from_list(lambda x: x, self.list) - if self.quota_snapshots is not None: - result["quotaSnapshots"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.quota_snapshots) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionOpenOptionsAdditionalContentExclusionPolicyRuleSource: @@ -7801,15 +8033,21 @@ class SessionUpdateOptionsResult: success: bool """Whether the operation succeeded""" + plugin_hook_count: int | None = None + """Number of hooks loaded from installed plugins, returned when installedPlugins is updated""" + @staticmethod def from_dict(obj: Any) -> 'SessionUpdateOptionsResult': assert isinstance(obj, dict) success = from_bool(obj.get("success")) - return SessionUpdateOptionsResult(success) + plugin_hook_count = from_union([from_int, from_none], obj.get("pluginHookCount")) + return SessionUpdateOptionsResult(success, plugin_hook_count) def to_dict(self) -> dict: result: dict = {} result["success"] = from_bool(self.success) + if self.plugin_hook_count is not None: + result["pluginHookCount"] = from_union([from_int, from_none], self.plugin_hook_count) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -10440,77 +10678,6 @@ def to_dict(self) -> dict: result["mode"] = from_union([lambda x: to_enum(PermissionsAllowAllMode, x), from_none], self.mode) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class DiscoveredCanvas: - """Canvas available in the current session.""" - - canvas_id: str - """Provider-local canvas identifier""" - - description: str - """Short, single-sentence description shown to the agent in canvas catalogs.""" - - display_name: str - """Human-readable canvas name""" - - extension_id: str - """Owning provider identifier""" - - actions: list[CanvasAction] | None = None - """Actions the agent or host may invoke on an open instance""" - - extension_name: str | None = None - """Owning extension display name, when available""" - - input_schema: Any = None - """JSON Schema for canvas open input""" - - @staticmethod - def from_dict(obj: Any) -> 'DiscoveredCanvas': - assert isinstance(obj, dict) - canvas_id = from_str(obj.get("canvasId")) - description = from_str(obj.get("description")) - display_name = from_str(obj.get("displayName")) - extension_id = from_str(obj.get("extensionId")) - actions = from_union([lambda x: from_list(CanvasAction.from_dict, x), from_none], obj.get("actions")) - extension_name = from_union([from_str, from_none], obj.get("extensionName")) - input_schema = obj.get("inputSchema") - return DiscoveredCanvas(canvas_id, description, display_name, extension_id, actions, extension_name, input_schema) - - def to_dict(self) -> dict: - result: dict = {} - result["canvasId"] = from_str(self.canvas_id) - result["description"] = from_str(self.description) - result["displayName"] = from_str(self.display_name) - result["extensionId"] = from_str(self.extension_id) - if self.actions is not None: - result["actions"] = from_union([lambda x: from_list(lambda x: to_class(CanvasAction, x), x), from_none], self.actions) - if self.extension_name is not None: - result["extensionName"] = from_union([from_str, from_none], self.extension_name) - if self.input_schema is not None: - result["inputSchema"] = self.input_schema - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class CanvasListOpenResult: - """Live open-canvas snapshot.""" - - open_canvases: list[OpenCanvasInstance] - """Currently open canvas instances""" - - @staticmethod - def from_dict(obj: Any) -> 'CanvasListOpenResult': - assert isinstance(obj, dict) - open_canvases = from_list(OpenCanvasInstance.from_dict, obj.get("openCanvases")) - return CanvasListOpenResult(open_canvases) - - def to_dict(self) -> dict: - result: dict = {} - result["openCanvases"] = from_list(lambda x: to_class(OpenCanvasInstance, x), self.open_canvases) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SlashCommandInput: @@ -10681,30 +10848,153 @@ def from_dict(obj: Any) -> 'MetadataContextHeaviestMessagesResult': total_tokens = from_int(obj.get("totalTokens")) return MetadataContextHeaviestMessagesResult(messages, total_tokens) - def to_dict(self) -> dict: - result: dict = {} - result["messages"] = from_list(lambda x: to_class(ContextHeaviestMessage, x), self.messages) - result["totalTokens"] = from_int(self.total_tokens) - return result + def to_dict(self) -> dict: + result: dict = {} + result["messages"] = from_list(lambda x: to_class(ContextHeaviestMessage, x), self.messages) + result["totalTokens"] = from_int(self.total_tokens) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CanvasHostContextCapabilities: + """Host capabilities""" + + canvases: bool | None = None + """Whether canvas rendering is supported""" + + @staticmethod + def from_dict(obj: Any) -> 'CanvasHostContextCapabilities': + assert isinstance(obj, dict) + canvases = from_union([from_bool, from_none], obj.get("canvases")) + return CanvasHostContextCapabilities(canvases) + + def to_dict(self) -> dict: + result: dict = {} + if self.canvases is not None: + result["canvases"] = from_union([from_bool, from_none], self.canvases) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class DiscoveredCanvas: + """Canvas available in the current session.""" + + canvas_id: str + """Provider-local canvas identifier""" + + description: str + """Short, single-sentence description shown to the agent in canvas catalogs.""" + + display_name: str + """Human-readable canvas name""" + + extension_id: str + """Owning provider identifier""" + + actions: list[CanvasAction] | None = None + """Actions the agent or host may invoke on an open instance""" + + extension_name: str | None = None + """Owning extension display name, when available""" + + icon: str | None = None + """Host-local PNG path for the canvas icon, when supplied""" + + input_schema: Any = None + """JSON Schema for canvas open input""" + + @staticmethod + def from_dict(obj: Any) -> 'DiscoveredCanvas': + assert isinstance(obj, dict) + canvas_id = from_str(obj.get("canvasId")) + description = from_str(obj.get("description")) + display_name = from_str(obj.get("displayName")) + extension_id = from_str(obj.get("extensionId")) + actions = from_union([lambda x: from_list(CanvasAction.from_dict, x), from_none], obj.get("actions")) + extension_name = from_union([from_str, from_none], obj.get("extensionName")) + icon = from_union([from_str, from_none], obj.get("icon")) + input_schema = obj.get("inputSchema") + return DiscoveredCanvas(canvas_id, description, display_name, extension_id, actions, extension_name, icon, input_schema) + + def to_dict(self) -> dict: + result: dict = {} + result["canvasId"] = from_str(self.canvas_id) + result["description"] = from_str(self.description) + result["displayName"] = from_str(self.display_name) + result["extensionId"] = from_str(self.extension_id) + if self.actions is not None: + result["actions"] = from_union([lambda x: from_list(lambda x: to_class(CanvasAction, x), x), from_none], self.actions) + if self.extension_name is not None: + result["extensionName"] = from_union([from_str, from_none], self.extension_name) + if self.icon is not None: + result["icon"] = from_union([from_str, from_none], self.icon) + if self.input_schema is not None: + result["inputSchema"] = self.input_schema + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class OpenCanvasInstance: + """Open canvas instance snapshot.""" + + canvas_id: str + """Provider-local canvas identifier""" + + extension_id: str + """Owning provider identifier""" + + instance_id: str + """Stable caller-supplied canvas instance identifier""" + + extension_name: str | None = None + """Owning extension display name, when available""" + + icon: str | None = None + """Host-local PNG path for the canvas icon, when supplied""" -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class CanvasHostContextCapabilities: - """Host capabilities""" + input: Any = None + """Input supplied when the instance was opened""" - canvases: bool | None = None - """Whether canvas rendering is supported""" + status: str | None = None + """Provider-supplied status text""" + + title: str | None = None + """Rendered title""" + + url: str | None = None + """URL for web-rendered canvases""" @staticmethod - def from_dict(obj: Any) -> 'CanvasHostContextCapabilities': + def from_dict(obj: Any) -> 'OpenCanvasInstance': assert isinstance(obj, dict) - canvases = from_union([from_bool, from_none], obj.get("canvases")) - return CanvasHostContextCapabilities(canvases) + canvas_id = from_str(obj.get("canvasId")) + extension_id = from_str(obj.get("extensionId")) + instance_id = from_str(obj.get("instanceId")) + extension_name = from_union([from_str, from_none], obj.get("extensionName")) + icon = from_union([from_str, from_none], obj.get("icon")) + input = obj.get("input") + status = from_union([from_str, from_none], obj.get("status")) + title = from_union([from_str, from_none], obj.get("title")) + url = from_union([from_str, from_none], obj.get("url")) + return OpenCanvasInstance(canvas_id, extension_id, instance_id, extension_name, icon, input, status, title, url) def to_dict(self) -> dict: result: dict = {} - if self.canvases is not None: - result["canvases"] = from_union([from_bool, from_none], self.canvases) + result["canvasId"] = from_str(self.canvas_id) + result["extensionId"] = from_str(self.extension_id) + result["instanceId"] = from_str(self.instance_id) + if self.extension_name is not None: + result["extensionName"] = from_union([from_str, from_none], self.extension_name) + if self.icon is not None: + result["icon"] = from_union([from_str, from_none], self.icon) + if self.input is not None: + result["input"] = self.input + if self.status is not None: + result["status"] = from_union([from_str, from_none], self.status) + if self.title is not None: + result["title"] = from_union([from_str, from_none], self.title) + if self.url is not None: + result["url"] = from_union([from_str, from_none], self.url) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -11043,6 +11333,49 @@ def to_dict(self) -> dict: ExternalToolTextResultForLlmContentResourceDetails = EmbeddedTextResourceContents | EmbeddedBlobResourceContents +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPResourceIcon: + """A resource icon descriptor plus preserved non-standard icon fields.""" + + src: str + """Icon URI""" + + additional_properties: dict[str, Any] | None = None + """Server-provided non-standard icon fields preserved from the MCP response""" + + mime_type: str | None = None + """Icon MIME type, when known""" + + sizes: str | None = None + """Icon sizes hint""" + + theme: str | None = None + """Theme hint for this icon""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPResourceIcon': + assert isinstance(obj, dict) + src = from_str(obj.get("src")) + additional_properties = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("additionalProperties")) + mime_type = from_union([from_str, from_none], obj.get("mimeType")) + sizes = from_union([from_str, from_none], obj.get("sizes")) + theme = from_union([from_str, from_none], obj.get("theme")) + return MCPResourceIcon(src, additional_properties, mime_type, sizes, theme) + + def to_dict(self) -> dict: + result: dict = {} + result["src"] = from_str(self.src) + if self.additional_properties is not None: + result["additionalProperties"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.additional_properties) + if self.mime_type is not None: + result["mimeType"] = from_union([from_str, from_none], self.mime_type) + if self.sizes is not None: + result["sizes"] = from_union([from_str, from_none], self.sizes) + if self.theme is not None: + result["theme"] = from_union([from_str, from_none], self.theme) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ExternalToolTextResultForLlmContentAudio: @@ -11314,6 +11647,30 @@ def to_dict(self) -> dict: result["summaryContent"] = from_union([from_str, from_none], self.summary_content) 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: @@ -12394,6 +12751,9 @@ def to_dict(self) -> dict: class MCPServerConfig: """MCP server configuration (stdio process or remote HTTP/SSE) + Replacement MCP server configuration (stdio process or remote HTTP/SSE). Omit to restart + the server with its already-registered configuration (config-free restart-by-name). + Stdio MCP server configuration launched as a child process. Remote MCP server configuration accessed over HTTP or SSE. @@ -12740,6 +13100,25 @@ def to_dict(self) -> dict: result["tokenType"] = from_union([from_str, from_none], self.token_type) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPResourcesReadResult: + """Resource contents returned by the MCP server.""" + + contents: list[MCPResourceContent] + """Resource contents returned by the server""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPResourcesReadResult': + assert isinstance(obj, dict) + contents = from_list(MCPResourceContent.from_dict, obj.get("contents")) + return MCPResourcesReadResult(contents) + + def to_dict(self) -> dict: + result: dict = {} + result["contents"] = from_list(lambda x: to_class(MCPResourceContent, x), self.contents) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPSamplingExecutionResult: @@ -13037,6 +13416,27 @@ def to_dict(self) -> dict: result["vision"] = from_union([lambda x: to_class(ModelCapabilitiesLimitsVision, x), from_none], self.vision) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionModelPriceCategory: + """Cost-category metadata for a CAPI model.""" + + id: str + price_category: ModelPickerPriceCategory + + @staticmethod + def from_dict(obj: Any) -> 'SessionModelPriceCategory': + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + price_category = ModelPickerPriceCategory(obj.get("priceCategory")) + return SessionModelPriceCategory(id, price_category) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + result["priceCategory"] = to_enum(ModelPickerPriceCategory, self.price_category) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ModelPolicy: @@ -15501,6 +15901,12 @@ class RemoteControlStatusActive: state: ClassVar[str] = "active" """Remote control state tag: active.""" + # Internal: this field is an internal SDK API and is not part of the public surface. + awaiting_first_message: bool | None = None + """True while a read-only/session-sync export is deferred, awaiting the first `user.message` + before its MC session exists. Marked internal: this field is excluded from the public SDK + surface and is populated only on the CLI in-process path. + """ frontend_url: str | None = None """MC frontend URL for this session, when known.""" @@ -15517,15 +15923,18 @@ def from_dict(obj: Any) -> 'RemoteControlStatusActive': assert isinstance(obj, dict) attached_session_id = from_str(obj.get("attachedSessionId")) is_steerable = from_bool(obj.get("isSteerable")) + awaiting_first_message = from_union([from_bool, from_none], obj.get("awaitingFirstMessage")) frontend_url = from_union([from_str, from_none], obj.get("frontendUrl")) prompt_manager = obj.get("promptManager") - return RemoteControlStatusActive(attached_session_id, is_steerable, frontend_url, prompt_manager) + return RemoteControlStatusActive(attached_session_id, is_steerable, awaiting_first_message, frontend_url, prompt_manager) def to_dict(self) -> dict: result: dict = {} result["attachedSessionId"] = from_str(self.attached_session_id) result["isSteerable"] = from_bool(self.is_steerable) result["state"] = self.state + if self.awaiting_first_message is not None: + result["awaitingFirstMessage"] = from_union([from_bool, from_none], self.awaiting_first_message) if self.frontend_url is not None: result["frontendUrl"] = from_union([from_str, from_none], self.frontend_url) if self.prompt_manager is not None: @@ -15687,6 +16096,77 @@ def to_dict(self) -> dict: result["entry"] = from_union([lambda x: to_class(ScheduleEntry, x), from_none], self.entry) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SendMessagesRequest: + """Parameters for sending zero or more user messages to the session in a single turn. + Remote-backed (Mission Control) sessions do not support this method and will return an + error. + """ + messages: list[SendMessageItem] + """The user messages to append to the conversation, in order. May be empty, in which case a + single turn runs over the existing history with no new user message. + """ + agent_mode: SendAgentMode | None = None + """The UI mode the agent was in when these messages were sent. Defaults to the session's + current mode. + """ + mode: SendMode | None = None + """How to deliver the messages. `enqueue` (default) appends to the message queue. + `immediate` interjects during an in-progress turn. + """ + prepend: bool | None = None + """If true, adds the messages to the front of the queue instead of the end""" + + request_headers: dict[str, str] | None = None + """Custom HTTP headers to include in outbound model requests for this turn. Merged with + session-level provider headers; per-turn headers augment and overwrite session-level + headers with the same key. + """ + traceparent: str | None = None + """W3C Trace Context traceparent header for distributed tracing of this agent turn""" + + tracestate: str | None = None + """W3C Trace Context tracestate header for distributed tracing""" + + wait: bool | None = None + """If true, await completion of the agentic loop for this turn before returning. Defaults to + false (fire-and-forget). When true, the result still contains the same `messageIds`; the + caller can rely on the agent having processed the messages before the call resolves. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SendMessagesRequest': + assert isinstance(obj, dict) + messages = from_list(SendMessageItem.from_dict, obj.get("messages")) + agent_mode = from_union([SendAgentMode, from_none], obj.get("agentMode")) + mode = from_union([SendMode, from_none], obj.get("mode")) + prepend = from_union([from_bool, from_none], obj.get("prepend")) + request_headers = from_union([lambda x: from_dict(from_str, x), from_none], obj.get("requestHeaders")) + traceparent = from_union([from_str, from_none], obj.get("traceparent")) + tracestate = from_union([from_str, from_none], obj.get("tracestate")) + wait = from_union([from_bool, from_none], obj.get("wait")) + return SendMessagesRequest(messages, agent_mode, mode, prepend, request_headers, traceparent, tracestate, wait) + + def to_dict(self) -> dict: + result: dict = {} + result["messages"] = from_list(lambda x: to_class(SendMessageItem, x), self.messages) + if self.agent_mode is not None: + result["agentMode"] = from_union([lambda x: to_enum(SendAgentMode, x), from_none], self.agent_mode) + if self.mode is not None: + result["mode"] = from_union([lambda x: to_enum(SendMode, x), from_none], self.mode) + if self.prepend is not None: + result["prepend"] = from_union([from_bool, from_none], self.prepend) + if self.request_headers is not None: + result["requestHeaders"] = from_union([lambda x: from_dict(from_str, x), from_none], self.request_headers) + if self.traceparent is not None: + result["traceparent"] = from_union([from_str, from_none], self.traceparent) + if self.tracestate is not None: + result["tracestate"] = from_union([from_str, from_none], self.tracestate) + if self.wait is not None: + result["wait"] = from_union([from_bool, from_none], self.wait) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SendRequest: @@ -16398,7 +16878,7 @@ def to_dict(self) -> dict: @dataclass class SlashCommandAgentPromptResult: """Slash-command invocation result that submits an agent prompt, with display prompt, - optional mode, and settings-change flag. + optional mode, optional user-facing notice, and settings-change flag. """ display_prompt: str """Prompt text to display to the user""" @@ -16412,6 +16892,9 @@ class SlashCommandAgentPromptResult: mode: SessionMode | None = None """Optional target session mode for the agent prompt""" + notice: str | None = None + """Optional user-facing notice to show before the prompt is submitted""" + runtime_settings_changed: bool | None = None """True when the invocation mutated user runtime settings; consumers caching settings should refresh @@ -16423,8 +16906,9 @@ def from_dict(obj: Any) -> 'SlashCommandAgentPromptResult': display_prompt = from_str(obj.get("displayPrompt")) prompt = from_str(obj.get("prompt")) mode = from_union([SessionMode, from_none], obj.get("mode")) + notice = from_union([from_str, from_none], obj.get("notice")) runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) - return SlashCommandAgentPromptResult(display_prompt, prompt, mode, runtime_settings_changed) + return SlashCommandAgentPromptResult(display_prompt, prompt, mode, notice, runtime_settings_changed) def to_dict(self) -> dict: result: dict = {} @@ -16433,6 +16917,8 @@ def to_dict(self) -> dict: result["prompt"] = from_str(self.prompt) if self.mode is not None: result["mode"] = from_union([lambda x: to_enum(SessionMode, x), from_none], self.mode) + if self.notice is not None: + result["notice"] = from_union([from_str, from_none], self.notice) if self.runtime_settings_changed is not None: result["runtimeSettingsChanged"] = from_union([from_bool, from_none], self.runtime_settings_changed) return result @@ -16740,27 +17226,32 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class MCPTools: - """MCP tool metadata with tool name and optional description.""" +class MCPToolUI: + """Normalized MCP Apps discovery metadata. An empty object indicates that a valid `_meta.ui` + block was present without recognized fields. - name: str - """Tool name.""" - - description: str | None = None - """Tool description, when provided.""" + Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block. + """ + resource_uri: str | None = None + """URI of the tool's MCP App resource, typically a `ui://` resource identifier. Use + `session.mcp.resources.read` to fetch its HTML and resource metadata. + """ + visibility: list[MCPToolUIVisibility] | None = None + """Tool visibility advertised by the server. When absent, MCP Apps defaults apply.""" @staticmethod - def from_dict(obj: Any) -> 'MCPTools': + def from_dict(obj: Any) -> 'MCPToolUI': assert isinstance(obj, dict) - name = from_str(obj.get("name")) - description = from_union([from_str, from_none], obj.get("description")) - return MCPTools(name, description) + resource_uri = from_union([from_str, from_none], obj.get("resourceUri")) + visibility = from_union([lambda x: from_list(MCPToolUIVisibility, x), from_none], obj.get("visibility")) + return MCPToolUI(resource_uri, visibility) def to_dict(self) -> dict: result: dict = {} - result["name"] = from_str(self.name) - if self.description is not None: - result["description"] = from_union([from_str, from_none], self.description) + if self.resource_uri is not None: + result["resourceUri"] = from_union([from_str, from_none], self.resource_uri) + if self.visibility is not None: + result["visibility"] = from_union([lambda x: from_list(lambda x: to_enum(MCPToolUIVisibility, x), x), from_none], self.visibility) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -17646,25 +18137,6 @@ 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 CanvasList: - """Declared canvases available in this session.""" - - canvases: list[DiscoveredCanvas] - """Declared canvases available in this session""" - - @staticmethod - def from_dict(obj: Any) -> 'CanvasList': - assert isinstance(obj, dict) - canvases = from_list(DiscoveredCanvas.from_dict, obj.get("canvases")) - return CanvasList(canvases) - - def to_dict(self) -> dict: - result: dict = {} - result["canvases"] = from_list(lambda x: to_class(DiscoveredCanvas, x), self.canvases) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SlashCommandInfo: @@ -17772,6 +18244,44 @@ def to_dict(self) -> dict: result["capabilities"] = from_union([lambda x: to_class(CanvasHostContextCapabilities, x), from_none], self.capabilities) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CanvasList: + """Declared canvases available in this session.""" + + canvases: list[DiscoveredCanvas] + """Declared canvases available in this session""" + + @staticmethod + def from_dict(obj: Any) -> 'CanvasList': + assert isinstance(obj, dict) + canvases = from_list(DiscoveredCanvas.from_dict, obj.get("canvases")) + return CanvasList(canvases) + + def to_dict(self) -> dict: + result: dict = {} + result["canvases"] = from_list(lambda x: to_class(DiscoveredCanvas, x), self.canvases) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CanvasListOpenResult: + """Live open-canvas snapshot.""" + + open_canvases: list[OpenCanvasInstance] + """Currently open canvas instances""" + + @staticmethod + def from_dict(obj: Any) -> 'CanvasListOpenResult': + assert isinstance(obj, dict) + open_canvases = from_list(OpenCanvasInstance.from_dict, obj.get("openCanvases")) + return CanvasListOpenResult(open_canvases) + + def to_dict(self) -> dict: + result: dict = {} + result["openCanvases"] = from_list(lambda x: to_class(OpenCanvasInstance, x), self.open_canvases) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class DebugCollectLogsResult: @@ -18782,54 +19292,54 @@ 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 MCPRestartServerRequest: - """Server name and opaque configuration for an individual MCP server restart.""" - + """Server name and optional replacement configuration for an individual MCP server restart. + Omit `config` for a config-free restart-by-name of an already-configured server. + """ server_name: str """Name of the MCP server to restart""" - config: Any = None - """Opaque server configuration (MCPServerConfig). Marked internal: an in-process runtime - shape supplied only by in-process CLI callers. + config: MCPServerConfig | None = None + """Replacement MCP server configuration (stdio process or remote HTTP/SSE). Omit to restart + the server with its already-registered configuration (config-free restart-by-name). """ + @staticmethod def from_dict(obj: Any) -> 'MCPRestartServerRequest': assert isinstance(obj, dict) - config = obj.get("config") server_name = from_str(obj.get("serverName")) - return MCPRestartServerRequest(config, server_name) + config = from_union([MCPServerConfig.from_dict, from_none], obj.get("config")) + return MCPRestartServerRequest(server_name, config) def to_dict(self) -> dict: result: dict = {} - result["config"] = self.config result["serverName"] = from_str(self.server_name) + if self.config is not None: + result["config"] = from_union([lambda x: to_class(MCPServerConfig, x), from_none], self.config) 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 MCPStartServerRequest: - """Server name and opaque configuration for an individual MCP server start.""" + """Server name and configuration for an individual MCP server start.""" + + config: MCPServerConfig + """MCP server configuration (stdio process or remote HTTP/SSE)""" server_name: str """Name of the MCP server to start""" - - config: Any = None - """Opaque server configuration (MCPServerConfig). Marked internal: an in-process runtime - shape supplied only by in-process CLI callers. - """ + @staticmethod def from_dict(obj: Any) -> 'MCPStartServerRequest': assert isinstance(obj, dict) - config = obj.get("config") + config = MCPServerConfig.from_dict(obj.get("config")) server_name = from_str(obj.get("serverName")) return MCPStartServerRequest(config, server_name) def to_dict(self) -> dict: result: dict = {} - result["config"] = self.config + result["config"] = to_class(MCPServerConfig, self.config) result["serverName"] = from_str(self.server_name) return result @@ -19027,6 +19537,10 @@ class ModelBilling: multiplier: float | None = None """Billing cost multiplier relative to the base rate""" + promo: ModelBillingPromo | None = None + """Active server-driven promotion for this model, if any. Present when the model is being + promoted with a time-boxed discount. + """ token_prices: ModelBillingTokenPrices | None = None """Token-level pricing information for this model""" @@ -19035,8 +19549,9 @@ def from_dict(obj: Any) -> 'ModelBilling': assert isinstance(obj, dict) discount_percent = from_union([from_int, from_none], obj.get("discountPercent")) multiplier = from_union([from_float, from_none], obj.get("multiplier")) + promo = from_union([ModelBillingPromo.from_dict, from_none], obj.get("promo")) token_prices = from_union([ModelBillingTokenPrices.from_dict, from_none], obj.get("tokenPrices")) - return ModelBilling(discount_percent, multiplier, token_prices) + return ModelBilling(discount_percent, multiplier, promo, token_prices) def to_dict(self) -> dict: result: dict = {} @@ -19044,10 +19559,46 @@ def to_dict(self) -> dict: result["discountPercent"] = from_union([from_int, from_none], self.discount_percent) if self.multiplier is not None: result["multiplier"] = from_union([to_float, from_none], self.multiplier) + if self.promo is not None: + result["promo"] = from_union([lambda x: to_class(ModelBillingPromo, x), from_none], self.promo) if self.token_prices is not None: result["tokenPrices"] = from_union([lambda x: to_class(ModelBillingTokenPrices, x), from_none], self.token_prices) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionModelList: + """The list of models available to this session.""" + + list: list[Any] + """Available models, ordered with the most preferred default first. Includes both Copilot + (CAPI) models and any registry BYOK models; a BYOK model appears under its + provider-qualified selection id (`provider/id`). + """ + model_price_categories: list[SessionModelPriceCategory] | None = None + """Cost categories for the full CAPI catalog, including picker-disabled models that Auto may + select. Metadata only; entries absent from `list` are not manually selectable. + """ + quota_snapshots: dict[str, Any] | None = None + """Per-quota snapshots returned alongside the model list, keyed by quota type.""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionModelList': + assert isinstance(obj, dict) + list = from_list(lambda x: x, obj.get("list")) + model_price_categories = from_union([lambda x: from_list(SessionModelPriceCategory.from_dict, x), from_none], obj.get("modelPriceCategories")) + quota_snapshots = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("quotaSnapshots")) + return SessionModelList(list, model_price_categories, quota_snapshots) + + def to_dict(self) -> dict: + result: dict = {} + result["list"] = from_list(lambda x: x, self.list) + if self.model_price_categories is not None: + result["modelPriceCategories"] = from_union([lambda x: from_list(lambda x: to_class(SessionModelPriceCategory, x), x), from_none], self.model_price_categories) + if self.quota_snapshots is not None: + result["quotaSnapshots"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.quota_snapshots) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ModelCapabilitiesOverride: @@ -19865,21 +20416,36 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class MCPListToolsResult: - """Tools exposed by the connected MCP server. Throws when the server is not connected.""" +class MCPTools: + """MCP tool metadata with tool name, optional description, and normalized MCP Apps discovery + metadata. + """ + name: str + """Tool name.""" - tools: list[MCPTools] - """Tools exposed by the server.""" + description: str | None = None + """Tool description, when provided.""" + + ui: MCPToolUI | None = None + """Normalized MCP Apps discovery metadata. An empty object indicates that a valid `_meta.ui` + block was present without recognized fields. + """ @staticmethod - def from_dict(obj: Any) -> 'MCPListToolsResult': + def from_dict(obj: Any) -> 'MCPTools': assert isinstance(obj, dict) - tools = from_list(MCPTools.from_dict, obj.get("tools")) - return MCPListToolsResult(tools) + name = from_str(obj.get("name")) + description = from_union([from_str, from_none], obj.get("description")) + ui = from_union([MCPToolUI.from_dict, from_none], obj.get("ui")) + return MCPTools(name, description, ui) def to_dict(self) -> dict: result: dict = {} - result["tools"] = from_list(lambda x: to_class(MCPTools, x), self.tools) + result["name"] = from_str(self.name) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + if self.ui is not None: + result["ui"] = from_union([lambda x: to_class(MCPToolUI, x), from_none], self.ui) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -21002,6 +21568,25 @@ def to_dict(self) -> dict: result["userPolicy"] = from_union([lambda x: to_class(SandboxConfigUserPolicy, x), from_none], self.user_policy) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPListToolsResult: + """Tools exposed by the connected MCP server. Throws when the server is not connected.""" + + tools: list[MCPTools] + """Tools exposed by the server.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPListToolsResult': + assert isinstance(obj, dict) + tools = from_list(MCPTools.from_dict, obj.get("tools")) + return MCPListToolsResult(tools) + + def to_dict(self) -> dict: + result: dict = {} + result["tools"] = from_list(lambda x: to_class(MCPTools, x), self.tools) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class UIElicitationSchema: @@ -21136,6 +21721,9 @@ class SessionOpenOptions: `assistant.message` event. Off by default; may change or be removed while the citations surface is experimental. """ + enable_managed_settings: bool | None = None + """Opt-in: self-fetch and enforce enterprise managed settings at session bootstrap.""" + enable_on_demand_instruction_discovery: bool | None = None """Whether on-demand custom instruction discovery is enabled.""" @@ -21169,6 +21757,11 @@ class SessionOpenOptions: feature_flags: dict[str, bool] | None = None """Feature-flag values resolved by the host.""" + 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 + same name remain available. + """ installed_plugins: list[InstalledPlugin] | None = None """Installed plugins visible to the session.""" @@ -21232,9 +21825,6 @@ class SessionOpenOptions: sandbox_config: SandboxConfig | None = None """Resolved sandbox configuration.""" - self_fetch_managed_settings: bool | None = None - """Opt-in: self-fetch enterprise managed settings at session bootstrap.""" - session_capabilities: list[SessionCapability] | None = None """Capabilities enabled for this session.""" @@ -21259,6 +21849,9 @@ class SessionOpenOptions: trajectory_file: str | None = None """Optional trajectory output file path.""" + verbosity: Verbosity | None = None + """Initial output verbosity level for supported models.""" + working_directory: str | None = None """Working directory to anchor the session.""" @@ -21287,6 +21880,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': disabled_instruction_sources = from_union([lambda x: from_list(from_str, x), from_none], obj.get("disabledInstructionSources")) disabled_skills = from_union([lambda x: from_list(from_str, x), from_none], obj.get("disabledSkills")) enable_citations = from_union([from_bool, from_none], obj.get("enableCitations")) + 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_streaming = from_union([from_bool, from_none], obj.get("enableStreaming")) @@ -21296,6 +21890,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")) + included_builtin_agents = from_union([lambda x: from_list(from_str, x), from_none], obj.get("includedBuiltinAgents")) installed_plugins = from_union([lambda x: from_list(InstalledPlugin.from_dict, x), from_none], obj.get("installedPlugins")) integration_id = from_union([from_str, from_none], obj.get("integrationId")) is_experimental_mode = from_union([from_bool, from_none], obj.get("isExperimentalMode")) @@ -21316,7 +21911,6 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': remote_steerable = from_union([from_bool, from_none], obj.get("remoteSteerable")) running_in_interactive_mode = from_union([from_bool, from_none], obj.get("runningInInteractiveMode")) sandbox_config = from_union([SandboxConfig.from_dict, from_none], obj.get("sandboxConfig")) - self_fetch_managed_settings = from_union([from_bool, from_none], obj.get("selfFetchManagedSettings")) session_capabilities = from_union([lambda x: from_list(SessionCapability, x), from_none], obj.get("sessionCapabilities")) session_id = from_union([from_str, from_none], obj.get("sessionId")) session_limits = from_union([SessionLimitsConfig.from_dict, from_none], obj.get("sessionLimits")) @@ -21325,9 +21919,10 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': skill_directories = from_union([lambda x: from_list(from_str, x), from_none], obj.get("skillDirectories")) skip_custom_instructions = from_union([from_bool, from_none], obj.get("skipCustomInstructions")) trajectory_file = from_union([from_str, from_none], obj.get("trajectoryFile")) + 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, 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_skills, enable_citations, enable_on_demand_instruction_discovery, enable_script_safety, enable_streaming, env_value_mode, events_log_directory, excluded_builtin_agents, excluded_tools, exp_assignments, feature_flags, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, 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, self_fetch_managed_settings, session_capabilities, session_id, session_limits, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, trajectory_file, working_directory, working_directory_context) + return SessionOpenOptions(additional_content_exclusion_policies, 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_skills, enable_citations, enable_managed_settings, enable_on_demand_instruction_discovery, enable_script_safety, enable_streaming, env_value_mode, events_log_directory, excluded_builtin_agents, excluded_tools, exp_assignments, feature_flags, included_builtin_agents, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, 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, session_capabilities, session_id, session_limits, 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 = {} @@ -21369,6 +21964,8 @@ def to_dict(self) -> dict: result["disabledSkills"] = from_union([lambda x: from_list(from_str, x), from_none], self.disabled_skills) if self.enable_citations is not None: result["enableCitations"] = from_union([from_bool, from_none], self.enable_citations) + if self.enable_managed_settings is not None: + result["enableManagedSettings"] = from_union([from_bool, from_none], self.enable_managed_settings) if self.enable_on_demand_instruction_discovery is not None: result["enableOnDemandInstructionDiscovery"] = from_union([from_bool, from_none], self.enable_on_demand_instruction_discovery) if self.enable_script_safety is not None: @@ -21387,6 +21984,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.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.installed_plugins is not None: result["installedPlugins"] = from_union([lambda x: from_list(lambda x: to_class(InstalledPlugin, x), x), from_none], self.installed_plugins) if self.integration_id is not None: @@ -21427,8 +22026,6 @@ def to_dict(self) -> dict: result["runningInInteractiveMode"] = from_union([from_bool, from_none], self.running_in_interactive_mode) if self.sandbox_config is not None: result["sandboxConfig"] = from_union([lambda x: to_class(SandboxConfig, x), from_none], self.sandbox_config) - if self.self_fetch_managed_settings is not None: - result["selfFetchManagedSettings"] = from_union([from_bool, from_none], self.self_fetch_managed_settings) if self.session_capabilities is not None: result["sessionCapabilities"] = from_union([lambda x: from_list(lambda x: to_enum(SessionCapability, x), x), from_none], self.session_capabilities) if self.session_id is not None: @@ -21445,6 +22042,8 @@ def to_dict(self) -> dict: result["skipCustomInstructions"] = from_union([from_bool, from_none], self.skip_custom_instructions) if self.trajectory_file is not None: result["trajectoryFile"] = from_union([from_str, from_none], self.trajectory_file) + if self.verbosity is not None: + result["verbosity"] = from_union([lambda x: to_enum(Verbosity, x), from_none], self.verbosity) if self.working_directory is not None: result["workingDirectory"] = from_union([from_str, from_none], self.working_directory) if self.working_directory_context is not None: @@ -21549,6 +22148,11 @@ class SessionUpdateOptionsParams: feature_flags: dict[str, bool] | None = None """Map of feature-flag IDs to their boolean enabled state.""" + 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 + same name remain available. Set to null to remove the allowlist restriction. + """ installed_plugins: list[SessionInstalledPlugin] | None = None """Full set of installed plugins for the session. Replaces the existing list; the runtime invalidates the skills cache only when the list materially changes. @@ -21635,6 +22239,9 @@ class SessionUpdateOptionsParams: trajectory_file: str | None = None """Optional path for trajectory output.""" + verbosity: Verbosity | None = None + """Output verbosity level for supported models.""" + working_directory: str | None = None """Absolute working-directory path for shell tools.""" @@ -21668,6 +22275,7 @@ def from_dict(obj: Any) -> 'SessionUpdateOptionsParams': excluded_builtin_agents = from_union([lambda x: from_list(from_str, x), from_none], obj.get("excludedBuiltinAgents")) excluded_tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("excludedTools")) feature_flags = from_union([lambda x: from_dict(from_bool, x), from_none], obj.get("featureFlags")) + included_builtin_agents = from_union([lambda x: from_list(from_str, x), from_none], obj.get("includedBuiltinAgents")) installed_plugins = from_union([lambda x: from_list(SessionInstalledPlugin.from_dict, x), from_none], obj.get("installedPlugins")) integration_id = from_union([from_str, from_none], obj.get("integrationId")) is_experimental_mode = from_union([from_bool, from_none], obj.get("isExperimentalMode")) @@ -21693,8 +22301,9 @@ def from_dict(obj: Any) -> 'SessionUpdateOptionsParams': suppress_custom_agent_prompt = from_union([from_bool, from_none], obj.get("suppressCustomAgentPrompt")) tool_filter_precedence = from_union([OptionsUpdateToolFilterPrecedence, from_none], obj.get("toolFilterPrecedence")) trajectory_file = from_union([from_str, from_none], obj.get("trajectoryFile")) + verbosity = from_union([Verbosity, from_none], obj.get("verbosity")) working_directory = from_union([from_str, from_none], obj.get("workingDirectory")) - return SessionUpdateOptionsParams(additional_content_exclusion_policies, agent_context, allow_all_mcp_server_instructions, ask_user_disabled, available_tools, capi, client_name, coauthor_enabled, context_tier, continue_on_auto_mode, copilot_url, custom_agents_local_only, disabled_instruction_sources, disabled_skills, enable_file_hooks, enable_host_git_operations, enable_on_demand_instruction_discovery, enable_reasoning_summaries, enable_script_safety, enable_session_store, enable_skills, enable_streaming, env_value_mode, events_log_directory, excluded_builtin_agents, excluded_tools, feature_flags, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, manage_schedule_enabled, max_inline_binary_bytes, model, model_capabilities_overrides, organization_custom_instructions, provider, reasoning_effort, reasoning_summary, running_in_interactive_mode, sandbox_config, session_capabilities, session_limits, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, skip_embedding_retrieval, suppress_custom_agent_prompt, tool_filter_precedence, trajectory_file, working_directory) + return SessionUpdateOptionsParams(additional_content_exclusion_policies, agent_context, allow_all_mcp_server_instructions, ask_user_disabled, available_tools, capi, client_name, coauthor_enabled, context_tier, continue_on_auto_mode, copilot_url, custom_agents_local_only, disabled_instruction_sources, disabled_skills, enable_file_hooks, enable_host_git_operations, enable_on_demand_instruction_discovery, enable_reasoning_summaries, enable_script_safety, enable_session_store, enable_skills, enable_streaming, env_value_mode, events_log_directory, excluded_builtin_agents, excluded_tools, feature_flags, included_builtin_agents, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, manage_schedule_enabled, max_inline_binary_bytes, model, model_capabilities_overrides, organization_custom_instructions, provider, reasoning_effort, reasoning_summary, running_in_interactive_mode, sandbox_config, session_capabilities, session_limits, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, skip_embedding_retrieval, suppress_custom_agent_prompt, tool_filter_precedence, trajectory_file, verbosity, working_directory) def to_dict(self) -> dict: result: dict = {} @@ -21752,6 +22361,8 @@ def to_dict(self) -> dict: result["excludedTools"] = from_union([lambda x: from_list(from_str, x), from_none], self.excluded_tools) 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.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.installed_plugins is not None: result["installedPlugins"] = from_union([lambda x: from_list(lambda x: to_class(SessionInstalledPlugin, x), x), from_none], self.installed_plugins) if self.integration_id is not None: @@ -21802,6 +22413,8 @@ def to_dict(self) -> dict: result["toolFilterPrecedence"] = from_union([lambda x: to_enum(OptionsUpdateToolFilterPrecedence, x), from_none], self.tool_filter_precedence) if self.trajectory_file is not None: result["trajectoryFile"] = from_union([from_str, from_none], self.trajectory_file) + if self.verbosity is not None: + result["verbosity"] = from_union([lambda x: to_enum(Verbosity, x), from_none], self.verbosity) if self.working_directory is not None: result["workingDirectory"] = from_union([from_str, from_none], self.working_directory) return result @@ -22820,6 +23433,241 @@ def to_dict(self) -> dict: result["transport"] = self.transport return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPResourceAnnotations: + """Model/client annotations associated with this resource + + Standard MCP resource annotations plus preserved non-standard annotation fields. + + Model/client annotations associated with this template + """ + additional_properties: dict[str, Any] | None = None + """Server-provided non-standard annotation fields preserved from the MCP response""" + + audience: list[str] | None = None + """Intended audience roles for this resource""" + + last_modified: str | None = None + """Last-modified timestamp hint""" + + priority: float | None = None + """Priority hint for model/client use""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPResourceAnnotations': + assert isinstance(obj, dict) + additional_properties = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("additionalProperties")) + audience = from_union([lambda x: from_list(from_str, x), from_none], obj.get("audience")) + last_modified = from_union([from_str, from_none], obj.get("lastModified")) + priority = from_union([from_float, from_none], obj.get("priority")) + return MCPResourceAnnotations(additional_properties, audience, last_modified, priority) + + def to_dict(self) -> dict: + result: dict = {} + if self.additional_properties is not None: + result["additionalProperties"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.additional_properties) + if self.audience is not None: + result["audience"] = from_union([lambda x: from_list(from_str, x), from_none], self.audience) + if self.last_modified is not None: + result["lastModified"] = from_union([from_str, from_none], self.last_modified) + if self.priority is not None: + result["priority"] = from_union([to_float, from_none], self.priority) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPResource: + """An MCP resource descriptor (spec `Resource`): URI, name, and optional title, description, + MIME type, size, icons, annotations, and metadata. Server-provided fields outside the + standard descriptor shape are exposed under `additionalProperties`. + """ + name: str + """The programmatic name of the resource""" + + uri: str + """The resource URI (e.g. ui://... or file:///...)""" + + meta: dict[str, Any] | None = None + """Resource-level metadata""" + + additional_properties: dict[str, Any] | None = None + """Server-provided non-standard descriptor fields preserved from the MCP response""" + + annotations: MCPResourceAnnotations | None = None + """Model/client annotations associated with this resource""" + + description: str | None = None + """Optional description of what this resource represents""" + + icons: list[MCPResourceIcon] | None = None + """Icons associated with this resource""" + + mime_type: str | None = None + """MIME type of the resource, if known""" + + size: int | None = None + """Resource size in bytes, when known""" + + title: str | None = None + """Optional human-readable display title""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPResource': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + uri = from_str(obj.get("uri")) + meta = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("_meta")) + additional_properties = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("additionalProperties")) + annotations = from_union([MCPResourceAnnotations.from_dict, from_none], obj.get("annotations")) + description = from_union([from_str, from_none], obj.get("description")) + icons = from_union([lambda x: from_list(MCPResourceIcon.from_dict, x), from_none], obj.get("icons")) + mime_type = from_union([from_str, from_none], obj.get("mimeType")) + size = from_union([from_int, from_none], obj.get("size")) + title = from_union([from_str, from_none], obj.get("title")) + return MCPResource(name, uri, meta, additional_properties, annotations, description, icons, mime_type, size, title) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + result["uri"] = from_str(self.uri) + if self.meta is not None: + result["_meta"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.meta) + if self.additional_properties is not None: + result["additionalProperties"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.additional_properties) + if self.annotations is not None: + result["annotations"] = from_union([lambda x: to_class(MCPResourceAnnotations, x), from_none], self.annotations) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + if self.icons is not None: + result["icons"] = from_union([lambda x: from_list(lambda x: to_class(MCPResourceIcon, x), x), from_none], self.icons) + if self.mime_type is not None: + result["mimeType"] = from_union([from_str, from_none], self.mime_type) + if self.size is not None: + result["size"] = from_union([from_int, from_none], self.size) + if self.title is not None: + result["title"] = from_union([from_str, from_none], self.title) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPResourceTemplate: + """An MCP resource template descriptor (spec `ResourceTemplate`): an RFC 6570 URI template, + name, and optional title, description, MIME type, icons, annotations, and metadata. + Server-provided fields outside the standard descriptor shape are exposed under + `additionalProperties`. + """ + name: str + """The programmatic name of the resource template""" + + uri_template: str + """An RFC 6570 URI template for constructing resource URIs""" + + meta: dict[str, Any] | None = None + """Resource-template-level metadata""" + + additional_properties: dict[str, Any] | None = None + """Server-provided non-standard descriptor fields preserved from the MCP response""" + + annotations: MCPResourceAnnotations | None = None + """Model/client annotations associated with this template""" + + description: str | None = None + """Optional description of what this template is for""" + + icons: list[MCPResourceIcon] | None = None + """Icons associated with resources matching this template""" + + mime_type: str | None = None + """MIME type for resources matching this template, if uniform""" + + title: str | None = None + """Optional human-readable display title""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPResourceTemplate': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + uri_template = from_str(obj.get("uriTemplate")) + meta = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("_meta")) + additional_properties = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("additionalProperties")) + annotations = from_union([MCPResourceAnnotations.from_dict, from_none], obj.get("annotations")) + description = from_union([from_str, from_none], obj.get("description")) + icons = from_union([lambda x: from_list(MCPResourceIcon.from_dict, x), from_none], obj.get("icons")) + mime_type = from_union([from_str, from_none], obj.get("mimeType")) + title = from_union([from_str, from_none], obj.get("title")) + return MCPResourceTemplate(name, uri_template, meta, additional_properties, annotations, description, icons, mime_type, title) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + result["uriTemplate"] = from_str(self.uri_template) + if self.meta is not None: + result["_meta"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.meta) + if self.additional_properties is not None: + result["additionalProperties"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.additional_properties) + if self.annotations is not None: + result["annotations"] = from_union([lambda x: to_class(MCPResourceAnnotations, x), from_none], self.annotations) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + if self.icons is not None: + result["icons"] = from_union([lambda x: from_list(lambda x: to_class(MCPResourceIcon, x), x), from_none], self.icons) + if self.mime_type is not None: + result["mimeType"] = from_union([from_str, from_none], self.mime_type) + if self.title is not None: + result["title"] = from_union([from_str, from_none], self.title) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPResourcesListResult: + """One page of resources advertised by the named MCP server.""" + + resources: list[MCPResource] + """Resources advertised by the server (proxied MCP `resources/list`)""" + + next_cursor: str | None = None + """Opaque cursor for the next page, if the server has more resources""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPResourcesListResult': + assert isinstance(obj, dict) + resources = from_list(MCPResource.from_dict, obj.get("resources")) + next_cursor = from_union([from_str, from_none], obj.get("nextCursor")) + return MCPResourcesListResult(resources, next_cursor) + + def to_dict(self) -> dict: + result: dict = {} + result["resources"] = from_list(lambda x: to_class(MCPResource, x), self.resources) + if self.next_cursor is not None: + result["nextCursor"] = from_union([from_str, from_none], self.next_cursor) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPResourcesListTemplatesResult: + """One page of resource templates advertised by the named MCP server.""" + + resource_templates: list[MCPResourceTemplate] + """Resource templates advertised by the server (proxied MCP `resources/templates/list`)""" + + next_cursor: str | None = None + """Opaque cursor for the next page, if the server has more resource templates""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPResourcesListTemplatesResult': + assert isinstance(obj, dict) + resource_templates = from_list(MCPResourceTemplate.from_dict, obj.get("resourceTemplates")) + next_cursor = from_union([from_str, from_none], obj.get("nextCursor")) + return MCPResourcesListTemplatesResult(resource_templates, next_cursor) + + def to_dict(self) -> dict: + result: dict = {} + result["resourceTemplates"] = from_list(lambda x: to_class(MCPResourceTemplate, x), self.resource_templates) + if self.next_cursor is not None: + result["nextCursor"] = from_union([from_str, from_none], self.next_cursor) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MetadataContextInfoRequest: @@ -23017,6 +23865,9 @@ class ModelSwitchToRequest: reasoning_summary: ReasoningSummary | None = None """Reasoning summary mode to request for supported model clients""" + verbosity: Verbosity | None = None + """Output verbosity level to request for supported models""" + @staticmethod def from_dict(obj: Any) -> 'ModelSwitchToRequest': assert isinstance(obj, dict) @@ -23025,7 +23876,8 @@ def from_dict(obj: Any) -> 'ModelSwitchToRequest': model_capabilities = from_union([ModelCapabilitiesOverride.from_dict, from_none], obj.get("modelCapabilities")) reasoning_effort = from_union([from_str, from_none], obj.get("reasoningEffort")) reasoning_summary = from_union([ReasoningSummary, from_none], obj.get("reasoningSummary")) - return ModelSwitchToRequest(model_id, context_tier, model_capabilities, reasoning_effort, reasoning_summary) + verbosity = from_union([Verbosity, from_none], obj.get("verbosity")) + return ModelSwitchToRequest(model_id, context_tier, model_capabilities, reasoning_effort, reasoning_summary, verbosity) def to_dict(self) -> dict: result: dict = {} @@ -23038,6 +23890,8 @@ def to_dict(self) -> dict: result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort) if self.reasoning_summary is not None: result["reasoningSummary"] = from_union([lambda x: to_enum(ReasoningSummary, x), from_none], self.reasoning_summary) + if self.verbosity is not None: + result["verbosity"] = from_union([lambda x: to_enum(Verbosity, x), from_none], self.verbosity) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -23220,6 +24074,15 @@ class SessionsOpenHandoff: `sessions.list` with `source: "remote"`). """ # Internal: this field is an internal SDK API and is not part of the public surface. + on_confirm: Any = None + """In-process confirmation callback `(request) => boolean | Promise` invoked when + the handoff needs the caller to confirm a non-fatal blocker (e.g. a repository mismatch + between the current working directory and the remote session). Returning `true` proceeds + with the handoff; returning `false` (or omitting the callback) aborts it. Marked internal + because a function reference cannot cross the JSON-RPC boundary, for the same reasons as + `onProgress`. + """ + # Internal: this field is an internal SDK API and is not part of the public surface. on_progress: Any = None """In-process progress callback `(update) => void` invoked for each handoff step. Marked internal because a function reference cannot cross the JSON-RPC boundary. The host-side @@ -23240,15 +24103,18 @@ class SessionsOpenHandoff: def from_dict(obj: Any) -> 'SessionsOpenHandoff': assert isinstance(obj, dict) metadata = RemoteSessionMetadataValue.from_dict(obj.get("metadata")) + on_confirm = obj.get("onConfirm") on_progress = obj.get("onProgress") options = from_union([SessionOpenOptions.from_dict, from_none], obj.get("options")) task_type = from_union([TaskType, from_none], obj.get("taskType")) - return SessionsOpenHandoff(metadata, on_progress, options, task_type) + return SessionsOpenHandoff(metadata, on_confirm, on_progress, options, task_type) def to_dict(self) -> dict: result: dict = {} result["kind"] = self.kind result["metadata"] = to_class(RemoteSessionMetadataValue, self.metadata) + if self.on_confirm is not None: + result["onConfirm"] = self.on_confirm if self.on_progress is not None: result["onProgress"] = self.on_progress if self.options is not None: @@ -23644,6 +24510,9 @@ class RPC: history_truncate_request: HistoryTruncateRequest history_truncate_result: HistoryTruncateResult hmac_auth_info: HMACAuthInfo + hook_invoke_request: _HookInvokeRequest + hook_invoke_response: _HookInvokeResponse + hook_type: _HookType installed_plugin: InstalledPlugin installed_plugin_info: InstalledPluginInfo installed_plugin_source: InstalledPluginSource | str @@ -23742,6 +24611,17 @@ class RPC: mcp_register_external_client_request: MCPRegisterExternalClientRequest mcp_reload_with_config_request: MCPReloadWithConfigRequest mcp_remove_git_hub_result: MCPRemoveGitHubResult + mcp_resource: MCPResource + mcp_resource_annotations: MCPResourceAnnotations + mcp_resource_content: MCPResourceContent + mcp_resource_icon: MCPResourceIcon + mcp_resources_list_request: MCPResourcesListRequest + mcp_resources_list_result: MCPResourcesListResult + mcp_resources_list_templates_request: MCPResourcesListTemplatesRequest + mcp_resources_list_templates_result: MCPResourcesListTemplatesResult + mcp_resources_read_request: MCPResourcesReadRequest + mcp_resources_read_result: MCPResourcesReadResult + mcp_resource_template: MCPResourceTemplate mcp_restart_server_request: MCPRestartServerRequest mcp_sampling_execution_action: MCPSamplingExecutionAction mcp_sampling_execution_result: MCPSamplingExecutionResult @@ -23764,6 +24644,8 @@ class RPC: mcp_start_servers_result: MCPStartServersResult mcp_stop_server_request: MCPStopServerRequest mcp_tools: MCPTools + mcp_tool_ui: MCPToolUI + mcp_tool_ui_visibility: MCPToolUIVisibility mcp_unregister_external_client_request: MCPUnregisterExternalClientRequest memory_configuration: MemoryConfiguration metadata_context_attribution_result: MetadataContextAttributionResult @@ -23784,6 +24666,7 @@ class RPC: metadata_snapshot_remote_metadata_task_type: TaskType model: Model model_billing: ModelBilling + model_billing_promo: ModelBillingPromo model_billing_token_prices: ModelBillingTokenPrices model_billing_token_prices_long_context: ModelBillingTokenPricesLongContext model_capabilities: ModelCapabilities @@ -24027,6 +24910,9 @@ class RPC: secrets_add_filter_values_result: SecretsAddFilterValuesResult send_agent_mode: SendAgentMode send_attachments_to_message_params: SendAttachmentsToMessageParams + send_message_item: SendMessageItem + send_messages_request: SendMessagesRequest + send_messages_result: SendMessagesResult send_mode: SendMode send_request: SendRequest send_result: SendResult @@ -24084,6 +24970,7 @@ class RPC: session_metadata_snapshot: SessionMetadataSnapshot session_mode: SessionMode session_model_list: SessionModelList + session_model_price_category: SessionModelPriceCategory session_open_options: SessionOpenOptions session_open_options_additional_content_exclusion_policy: SessionOpenOptionsAdditionalContentExclusionPolicy session_open_options_additional_content_exclusion_policy_rule: SessionOpenOptionsAdditionalContentExclusionPolicyRule @@ -24474,6 +25361,9 @@ def from_dict(obj: Any) -> 'RPC': history_truncate_request = HistoryTruncateRequest.from_dict(obj.get("HistoryTruncateRequest")) history_truncate_result = HistoryTruncateResult.from_dict(obj.get("HistoryTruncateResult")) 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")) 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")) @@ -24572,6 +25462,17 @@ def from_dict(obj: Any) -> 'RPC': mcp_register_external_client_request = MCPRegisterExternalClientRequest.from_dict(obj.get("McpRegisterExternalClientRequest")) mcp_reload_with_config_request = MCPReloadWithConfigRequest.from_dict(obj.get("McpReloadWithConfigRequest")) mcp_remove_git_hub_result = MCPRemoveGitHubResult.from_dict(obj.get("McpRemoveGitHubResult")) + mcp_resource = MCPResource.from_dict(obj.get("McpResource")) + mcp_resource_annotations = MCPResourceAnnotations.from_dict(obj.get("McpResourceAnnotations")) + mcp_resource_content = MCPResourceContent.from_dict(obj.get("McpResourceContent")) + mcp_resource_icon = MCPResourceIcon.from_dict(obj.get("McpResourceIcon")) + mcp_resources_list_request = MCPResourcesListRequest.from_dict(obj.get("McpResourcesListRequest")) + mcp_resources_list_result = MCPResourcesListResult.from_dict(obj.get("McpResourcesListResult")) + mcp_resources_list_templates_request = MCPResourcesListTemplatesRequest.from_dict(obj.get("McpResourcesListTemplatesRequest")) + mcp_resources_list_templates_result = MCPResourcesListTemplatesResult.from_dict(obj.get("McpResourcesListTemplatesResult")) + mcp_resources_read_request = MCPResourcesReadRequest.from_dict(obj.get("McpResourcesReadRequest")) + mcp_resources_read_result = MCPResourcesReadResult.from_dict(obj.get("McpResourcesReadResult")) + mcp_resource_template = MCPResourceTemplate.from_dict(obj.get("McpResourceTemplate")) mcp_restart_server_request = MCPRestartServerRequest.from_dict(obj.get("McpRestartServerRequest")) mcp_sampling_execution_action = MCPSamplingExecutionAction(obj.get("McpSamplingExecutionAction")) mcp_sampling_execution_result = MCPSamplingExecutionResult.from_dict(obj.get("McpSamplingExecutionResult")) @@ -24594,6 +25495,8 @@ def from_dict(obj: Any) -> 'RPC': mcp_start_servers_result = MCPStartServersResult.from_dict(obj.get("McpStartServersResult")) mcp_stop_server_request = MCPStopServerRequest.from_dict(obj.get("McpStopServerRequest")) mcp_tools = MCPTools.from_dict(obj.get("McpTools")) + mcp_tool_ui = MCPToolUI.from_dict(obj.get("McpToolUi")) + mcp_tool_ui_visibility = MCPToolUIVisibility(obj.get("McpToolUiVisibility")) mcp_unregister_external_client_request = MCPUnregisterExternalClientRequest.from_dict(obj.get("McpUnregisterExternalClientRequest")) memory_configuration = MemoryConfiguration.from_dict(obj.get("MemoryConfiguration")) metadata_context_attribution_result = MetadataContextAttributionResult.from_dict(obj.get("MetadataContextAttributionResult")) @@ -24614,6 +25517,7 @@ def from_dict(obj: Any) -> 'RPC': metadata_snapshot_remote_metadata_task_type = TaskType(obj.get("MetadataSnapshotRemoteMetadataTaskType")) model = Model.from_dict(obj.get("Model")) model_billing = ModelBilling.from_dict(obj.get("ModelBilling")) + model_billing_promo = ModelBillingPromo.from_dict(obj.get("ModelBillingPromo")) model_billing_token_prices = ModelBillingTokenPrices.from_dict(obj.get("ModelBillingTokenPrices")) model_billing_token_prices_long_context = ModelBillingTokenPricesLongContext.from_dict(obj.get("ModelBillingTokenPricesLongContext")) model_capabilities = ModelCapabilities.from_dict(obj.get("ModelCapabilities")) @@ -24857,6 +25761,9 @@ def from_dict(obj: Any) -> 'RPC': secrets_add_filter_values_result = SecretsAddFilterValuesResult.from_dict(obj.get("SecretsAddFilterValuesResult")) send_agent_mode = SendAgentMode(obj.get("SendAgentMode")) send_attachments_to_message_params = SendAttachmentsToMessageParams.from_dict(obj.get("SendAttachmentsToMessageParams")) + send_message_item = SendMessageItem.from_dict(obj.get("SendMessageItem")) + send_messages_request = SendMessagesRequest.from_dict(obj.get("SendMessagesRequest")) + send_messages_result = SendMessagesResult.from_dict(obj.get("SendMessagesResult")) send_mode = SendMode(obj.get("SendMode")) send_request = SendRequest.from_dict(obj.get("SendRequest")) send_result = SendResult.from_dict(obj.get("SendResult")) @@ -24914,6 +25821,7 @@ def from_dict(obj: Any) -> 'RPC': session_metadata_snapshot = SessionMetadataSnapshot.from_dict(obj.get("SessionMetadataSnapshot")) session_mode = SessionMode(obj.get("SessionMode")) session_model_list = SessionModelList.from_dict(obj.get("SessionModelList")) + session_model_price_category = SessionModelPriceCategory.from_dict(obj.get("SessionModelPriceCategory")) session_open_options = SessionOpenOptions.from_dict(obj.get("SessionOpenOptions")) session_open_options_additional_content_exclusion_policy = SessionOpenOptionsAdditionalContentExclusionPolicy.from_dict(obj.get("SessionOpenOptionsAdditionalContentExclusionPolicy")) session_open_options_additional_content_exclusion_policy_rule = SessionOpenOptionsAdditionalContentExclusionPolicyRule.from_dict(obj.get("SessionOpenOptionsAdditionalContentExclusionPolicyRule")) @@ -25140,7 +26048,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_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, agents_get_discovery_paths_request, allow_all_permission_set_result, allow_all_permission_state, api_key_auth_info, auth_info, auth_info_type, 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_session_context, capi_session_options, command_list, commands_handle_pending_command_request, commands_handle_pending_command_result, 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, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, 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_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_result, execute_command_params, execute_command_result, extension, extension_context_push_input, 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, 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, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_compact_context_window, history_compact_request, history_compact_result, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, 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, 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, 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_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, 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_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, 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_register_external_client_request, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_restart_server_request, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, 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_stdio, 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_tools, 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_billing, 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_picker_category, model_picker_price_category, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_to_request, model_switch_to_result, mode_set_request, 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_management, permission_decision_approve_for_location_approval_extension_permission_access, 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_management, permission_decision_approve_for_session_approval_extension_permission_access, 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_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_reject, permission_decision_request, 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_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_rules_set, permissions_allow_all_mode, 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_allow_all_request, 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_management, permissions_locations_add_tool_approval_details_extension_permission_access, 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_allow_all_request, permissions_set_allow_all_source, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, 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_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, 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, queued_command_handled, queued_command_not_handled, queued_command_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_most_recent_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_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, sandbox_config, 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_seatbelt, schedule_entry, schedule_list, 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_mode, send_request, send_result, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_auth_status, session_bulk_delete_result, session_capability, 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_stat_request, session_fs_stat_result, session_fs_write_file_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_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, 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_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, 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_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, 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, shell_cancel_user_requested_request, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_kill_request, shell_kill_result, shell_kill_signal, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skills_config_set_disabled_skills_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_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_select_subcommand_option, slash_command_select_subcommand_result, slash_command_text_result, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, 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, tool, tool_list, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_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_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_checkpoints, workspaces_create_file_request, workspaces_diff_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_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, workspace_summary_host_type, workspaces_workspace_details_host_type, 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_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, agents_get_discovery_paths_request, allow_all_permission_set_result, allow_all_permission_state, api_key_auth_info, auth_info, auth_info_type, 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_session_context, capi_session_options, command_list, commands_handle_pending_command_request, commands_handle_pending_command_result, 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, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, 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_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_result, execute_command_params, execute_command_result, extension, extension_context_push_input, 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, 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, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_compact_context_window, history_compact_request, history_compact_result, 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, 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, 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_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, 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_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, 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_register_external_client_request, 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_sampling_execution_action, mcp_sampling_execution_result, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, 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_stdio, 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_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_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_picker_category, model_picker_price_category, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_to_request, model_switch_to_result, mode_set_request, 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_management, permission_decision_approve_for_location_approval_extension_permission_access, 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_management, permission_decision_approve_for_session_approval_extension_permission_access, 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_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_reject, permission_decision_request, 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_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_rules_set, permissions_allow_all_mode, 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_allow_all_request, 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_management, permissions_locations_add_tool_approval_details_extension_permission_access, 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_allow_all_request, permissions_set_allow_all_source, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, 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_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, 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, queued_command_handled, queued_command_not_handled, queued_command_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_most_recent_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_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, sandbox_config, 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_seatbelt, schedule_entry, schedule_list, 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, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_auth_status, session_bulk_delete_result, session_capability, 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_stat_request, session_fs_stat_result, session_fs_write_file_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_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, 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_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, 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_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, 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, shell_cancel_user_requested_request, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_kill_request, shell_kill_result, shell_kill_signal, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skills_config_set_disabled_skills_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_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_select_subcommand_option, slash_command_select_subcommand_result, slash_command_text_result, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, 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, tool, tool_list, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_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_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_checkpoints, workspaces_create_file_request, workspaces_diff_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_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, workspace_summary_host_type, workspaces_workspace_details_host_type, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) def to_dict(self) -> dict: result: dict = {} @@ -25304,6 +26212,9 @@ def to_dict(self) -> dict: result["HistoryTruncateRequest"] = to_class(HistoryTruncateRequest, self.history_truncate_request) result["HistoryTruncateResult"] = to_class(HistoryTruncateResult, self.history_truncate_result) 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["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) @@ -25402,6 +26313,17 @@ def to_dict(self) -> dict: result["McpRegisterExternalClientRequest"] = to_class(MCPRegisterExternalClientRequest, self.mcp_register_external_client_request) result["McpReloadWithConfigRequest"] = to_class(MCPReloadWithConfigRequest, self.mcp_reload_with_config_request) result["McpRemoveGitHubResult"] = to_class(MCPRemoveGitHubResult, self.mcp_remove_git_hub_result) + result["McpResource"] = to_class(MCPResource, self.mcp_resource) + result["McpResourceAnnotations"] = to_class(MCPResourceAnnotations, self.mcp_resource_annotations) + result["McpResourceContent"] = to_class(MCPResourceContent, self.mcp_resource_content) + result["McpResourceIcon"] = to_class(MCPResourceIcon, self.mcp_resource_icon) + result["McpResourcesListRequest"] = to_class(MCPResourcesListRequest, self.mcp_resources_list_request) + result["McpResourcesListResult"] = to_class(MCPResourcesListResult, self.mcp_resources_list_result) + result["McpResourcesListTemplatesRequest"] = to_class(MCPResourcesListTemplatesRequest, self.mcp_resources_list_templates_request) + result["McpResourcesListTemplatesResult"] = to_class(MCPResourcesListTemplatesResult, self.mcp_resources_list_templates_result) + result["McpResourcesReadRequest"] = to_class(MCPResourcesReadRequest, self.mcp_resources_read_request) + result["McpResourcesReadResult"] = to_class(MCPResourcesReadResult, self.mcp_resources_read_result) + result["McpResourceTemplate"] = to_class(MCPResourceTemplate, self.mcp_resource_template) result["McpRestartServerRequest"] = to_class(MCPRestartServerRequest, self.mcp_restart_server_request) result["McpSamplingExecutionAction"] = to_enum(MCPSamplingExecutionAction, self.mcp_sampling_execution_action) result["McpSamplingExecutionResult"] = to_class(MCPSamplingExecutionResult, self.mcp_sampling_execution_result) @@ -25424,6 +26346,8 @@ def to_dict(self) -> dict: result["McpStartServersResult"] = to_class(MCPStartServersResult, self.mcp_start_servers_result) result["McpStopServerRequest"] = to_class(MCPStopServerRequest, self.mcp_stop_server_request) result["McpTools"] = to_class(MCPTools, self.mcp_tools) + result["McpToolUi"] = to_class(MCPToolUI, self.mcp_tool_ui) + result["McpToolUiVisibility"] = to_enum(MCPToolUIVisibility, self.mcp_tool_ui_visibility) result["McpUnregisterExternalClientRequest"] = to_class(MCPUnregisterExternalClientRequest, self.mcp_unregister_external_client_request) result["MemoryConfiguration"] = to_class(MemoryConfiguration, self.memory_configuration) result["MetadataContextAttributionResult"] = to_class(MetadataContextAttributionResult, self.metadata_context_attribution_result) @@ -25444,6 +26368,7 @@ def to_dict(self) -> dict: result["MetadataSnapshotRemoteMetadataTaskType"] = to_enum(TaskType, self.metadata_snapshot_remote_metadata_task_type) result["Model"] = to_class(Model, self.model) result["ModelBilling"] = to_class(ModelBilling, self.model_billing) + result["ModelBillingPromo"] = to_class(ModelBillingPromo, self.model_billing_promo) result["ModelBillingTokenPrices"] = to_class(ModelBillingTokenPrices, self.model_billing_token_prices) result["ModelBillingTokenPricesLongContext"] = to_class(ModelBillingTokenPricesLongContext, self.model_billing_token_prices_long_context) result["ModelCapabilities"] = to_class(ModelCapabilities, self.model_capabilities) @@ -25687,6 +26612,9 @@ def to_dict(self) -> dict: result["SecretsAddFilterValuesResult"] = to_class(SecretsAddFilterValuesResult, self.secrets_add_filter_values_result) result["SendAgentMode"] = to_enum(SendAgentMode, self.send_agent_mode) result["SendAttachmentsToMessageParams"] = to_class(SendAttachmentsToMessageParams, self.send_attachments_to_message_params) + result["SendMessageItem"] = to_class(SendMessageItem, self.send_message_item) + result["SendMessagesRequest"] = to_class(SendMessagesRequest, self.send_messages_request) + result["SendMessagesResult"] = to_class(SendMessagesResult, self.send_messages_result) result["SendMode"] = to_enum(SendMode, self.send_mode) result["SendRequest"] = to_class(SendRequest, self.send_request) result["SendResult"] = to_class(SendResult, self.send_result) @@ -25744,6 +26672,7 @@ def to_dict(self) -> dict: result["SessionMetadataSnapshot"] = to_class(SessionMetadataSnapshot, self.session_metadata_snapshot) result["SessionMode"] = to_enum(SessionMode, self.session_mode) result["SessionModelList"] = to_class(SessionModelList, self.session_model_list) + result["SessionModelPriceCategory"] = to_class(SessionModelPriceCategory, self.session_model_price_category) result["SessionOpenOptions"] = to_class(SessionOpenOptions, self.session_open_options) result["SessionOpenOptionsAdditionalContentExclusionPolicy"] = to_class(SessionOpenOptionsAdditionalContentExclusionPolicy, self.session_open_options_additional_content_exclusion_policy) result["SessionOpenOptionsAdditionalContentExclusionPolicyRule"] = to_class(SessionOpenOptionsAdditionalContentExclusionPolicyRule, self.session_open_options_additional_content_exclusion_policy_rule) @@ -26401,7 +27330,7 @@ async def list(self, *, timeout: float | None = None) -> MarketplaceListResult: return MarketplaceListResult.from_dict(await self._client.request("plugins.marketplaces.list", {}, **_timeout_kwargs(timeout))) async def add(self, params: PluginsMarketplacesAddRequest, *, timeout: float | None = None) -> MarketplaceAddResult: - "Registers a new marketplace from a source (owner/repo, URL, or local path).\n\nArgs:\n params: Marketplace source to register.\n\nReturns:\n Result of registering a new marketplace." + "Registers a new marketplace from a source (owner/repo, URL, or local path).\n\nArgs:\n params: Marketplace source and optional working directory for relative-path resolution.\n\nReturns:\n Result of registering a new marketplace." params_dict = {k: v for k, v in params.to_dict().items() if v is not None} return MarketplaceAddResult.from_dict(await self._client.request("plugins.marketplaces.add", params_dict, **_timeout_kwargs(timeout))) @@ -26521,6 +27450,16 @@ async def get_discovery_paths(self, params: InstructionsGetDiscoveryPathsRequest return InstructionDiscoveryPathList.from_dict(await self._client.request("instructions.getDiscoveryPaths", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class ServerCommandsApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def list(self, *, timeout: float | None = None) -> CommandList: + "Lists the well-known built-in slash commands that work as the first message in a new session (e.g. /plan, /env), without requiring an active session. Commands that depend on session state, authentication, or a synced session are omitted.\n\nReturns:\n Slash commands available in the session, after applying any include/exclude filters." + return CommandList.from_dict(await self._client.request("commands.list", {}, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class ServerUserSettingsApi: def __init__(self, client: "JsonRpcClient"): @@ -26731,6 +27670,7 @@ def __init__(self, client: "JsonRpcClient"): self.skills = ServerSkillsApi(client) self.agents = ServerAgentsApi(client) self.instructions = ServerInstructionsApi(client) + self.commands = ServerCommandsApi(client) self.user = ServerUserApi(client) self.runtime = ServerRuntimeApi(client) self.session_fs = ServerSessionFsApi(client) @@ -27246,6 +28186,31 @@ async def diagnose(self, params: MCPAppsDiagnoseRequest, *, timeout: float | Non return MCPAppsDiagnoseResult.from_dict(await self._client.request("session.mcp.apps.diagnose", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class McpResourcesApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def read(self, params: MCPResourcesReadRequest, *, timeout: float | None = None) -> MCPResourcesReadResult: + "Fetch an MCP resource from a connected server by URI (proxies MCP `resources/read`).\n\nArgs:\n params: MCP server and resource URI to fetch.\n\nReturns:\n Resource contents returned by the MCP server." + 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 MCPResourcesReadResult.from_dict(await self._client.request("session.mcp.resources.read", params_dict, **_timeout_kwargs(timeout))) + + async def list(self, params: MCPResourcesListRequest, *, timeout: float | None = None) -> MCPResourcesListResult: + "Enumerate one page of resources a connected MCP server exposes (proxies MCP `resources/list`). Pass `cursor` to continue from a prior result's `nextCursor`.\n\nArgs:\n params: MCP server whose resources to enumerate.\n\nReturns:\n One page of resources advertised by the named MCP server." + 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 MCPResourcesListResult.from_dict(await self._client.request("session.mcp.resources.list", params_dict, **_timeout_kwargs(timeout))) + + async def list_templates(self, params: MCPResourcesListTemplatesRequest, *, timeout: float | None = None) -> MCPResourcesListTemplatesResult: + "Enumerate one page of resource templates a connected MCP server exposes (proxies MCP `resources/templates/list`). Pass `cursor` to continue from a prior result's `nextCursor`.\n\nArgs:\n params: MCP server whose resource templates to enumerate.\n\nReturns:\n One page of resource templates advertised by the named MCP server." + 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 MCPResourcesListTemplatesResult.from_dict(await self._client.request("session.mcp.resources.listTemplates", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class McpApi: def __init__(self, client: "JsonRpcClient", session_id: str): @@ -27254,13 +28219,14 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.oauth = McpOauthApi(client, session_id) self.headers = McpHeadersApi(client, session_id) self.apps = McpAppsApi(client, session_id) + self.resources = McpResourcesApi(client, session_id) async def list(self, *, timeout: float | None = None) -> MCPServerList: "Lists MCP servers configured for the session, their connection status, and host-level state. The host-level state (disabled/filtered servers, failed/needs-auth/pending connections, mcp3p policy, full config) is empty/zero when no MCP host has been initialized for the session.\n\nReturns:\n MCP servers configured for the session, with their connection status and host-level state." return MCPServerList.from_dict(await self._client.request("session.mcp.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) async def list_tools(self, params: MCPListToolsRequest, *, timeout: float | None = None) -> MCPListToolsResult: - "Lists the tools exposed by a connected MCP server on this session's host.\n\nArgs:\n params: Server name whose tool list should be returned.\n\nReturns:\n Tools exposed by the connected MCP server. Throws when the server is not connected." + "Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session.\n\nArgs:\n params: Server name whose tool list should be returned.\n\nReturns:\n Tools exposed by the connected MCP server. Throws when the server is not connected." 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 MCPListToolsResult.from_dict(await self._client.request("session.mcp.listTools", params_dict, **_timeout_kwargs(timeout))) @@ -27303,6 +28269,18 @@ async def remove_git_hub(self, *, timeout: float | None = None) -> MCPRemoveGitH "Removes the auto-managed `github` MCP server when present.\n\nReturns:\n Indicates whether the auto-managed `github` MCP server was removed (false when nothing to remove)." return MCPRemoveGitHubResult.from_dict(await self._client.request("session.mcp.removeGitHub", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + async def start_server(self, params: MCPStartServerRequest, *, timeout: float | None = None) -> None: + "Starts an individual MCP server on the live session from a caller-supplied config. Session-scoped and ephemeral: the server is added to this session's running set only and is reaped when the session ends. Does NOT modify persistent user configuration (`mcp.config.*`), so it does not affect future sessions. The server surfaces through `session.mcp.list` and the `session.mcp_servers_loaded` / `session.mcp_server_status_changed` events like any other server.\n\nArgs:\n params: Server name and configuration for an individual MCP server start." + 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 + await self._client.request("session.mcp.startServer", params_dict, **_timeout_kwargs(timeout)) + + async def restart_server(self, params: MCPRestartServerRequest, *, timeout: float | None = None) -> None: + "Restarts an individual MCP server on the live session (stops then starts). Omit `config` for a config-free restart-by-name of an already-configured server; supply `config` to restart with a replacement configuration. Session-scoped and ephemeral: does NOT modify persistent user configuration (`mcp.config.*`).\n\nArgs:\n params: Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server." + 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 + await self._client.request("session.mcp.restartServer", params_dict, **_timeout_kwargs(timeout)) + async def stop_server(self, params: MCPStopServerRequest, *, timeout: float | None = None) -> None: "Stops an individual MCP server on the session's host.\n\nArgs:\n params: Server name for an individual MCP server stop." params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} @@ -27761,7 +28739,7 @@ async def record_context_change(self, params: MetadataRecordContextChangeRequest return MetadataRecordContextChangeResult.from_dict(await self._client.request("session.metadata.recordContextChange", params_dict, **_timeout_kwargs(timeout))) async def set_working_directory(self, params: MetadataSetWorkingDirectoryRequest, *, timeout: float | None = None) -> MetadataSetWorkingDirectoryResult: - "Updates the session's recorded working directory.\n\nArgs:\n params: Absolute path to set as the session's new working directory.\n\nReturns:\n Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for `process.chdir` and any related side-effects (file index, etc.); this method only updates the session's own recorded path." + "Updates the session's working directory. For local sessions the target is validated first (an absolute path that exists on disk) and the permission primary directory is re-based; a rejected validation fails the call before any session state changes.\n\nArgs:\n params: Absolute path to set as the session's new working directory. For local sessions the path must be absolute and exist on disk: it is validated before any session state changes, and a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote sessions record the path as-is.\n\nReturns:\n Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects (file index, etc.); it does NOT change the process working directory (a session's cwd is per-session, not process-global). For local sessions the runtime validates the target first (an absolute path that exists on disk) and re-bases the permission primary directory; a rejected validation fails the call before anything is mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the new directory (best-effort). Remote sessions only record the path." 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 MetadataSetWorkingDirectoryResult.from_dict(await self._client.request("session.metadata.setWorkingDirectory", params_dict, **_timeout_kwargs(timeout))) @@ -28001,6 +28979,12 @@ async def send(self, params: SendRequest, *, timeout: float | None = None) -> Se params_dict["sessionId"] = self._session_id return SendResult.from_dict(await self._client.request("session.send", params_dict, **_timeout_kwargs(timeout))) + async def send_messages(self, params: SendMessagesRequest, *, timeout: float | None = None) -> SendMessagesResult: + "Sends zero or more user messages to the session in a single turn and returns their message IDs. All provided messages are appended to the conversation in order, then exactly one agent turn runs over the resulting history. When the list is empty, one turn runs over the existing history with no new user message. Remote-backed (Mission Control) sessions do not support this method and will return an error.\n\nArgs:\n params: Parameters for sending zero or more user messages to the session in a single turn. Remote-backed (Mission Control) sessions do not support this method and will return an error.\n\nReturns:\n Result of sending zero or more user messages\n\n.. warning:: This API is experimental and may change or be removed in future versions." + 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 SendMessagesResult.from_dict(await self._client.request("session.sendMessages", params_dict, **_timeout_kwargs(timeout))) + async def abort(self, params: AbortRequest, *, timeout: float | None = None) -> AbortResult: "Aborts the current agent turn.\n\nArgs:\n params: Parameters for aborting the current turn\n\nReturns:\n Result of aborting the current turn\n\n.. warning:: This API is experimental and may change or be removed in future versions." params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} @@ -28038,18 +29022,6 @@ async def _configure_git_hub(self, params: MCPConfigureGitHubRequest, *, timeout params_dict["sessionId"] = self._session_id return MCPConfigureGitHubResult.from_dict(await self._client.request("session.mcp.configureGitHub", params_dict, **_timeout_kwargs(timeout))) - async def _start_server(self, params: MCPStartServerRequest, *, timeout: float | None = None) -> None: - "Starts an individual MCP server on the session's host.\n\nArgs:\n params: Server name and opaque configuration for an individual MCP server start.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." - 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 - await self._client.request("session.mcp.startServer", params_dict, **_timeout_kwargs(timeout)) - - async def _restart_server(self, params: MCPRestartServerRequest, *, timeout: float | None = None) -> None: - "Restarts an individual MCP server on the session's host (stops then starts).\n\nArgs:\n params: Server name and opaque configuration for an individual MCP server restart.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." - 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 - await self._client.request("session.mcp.restartServer", params_dict, **_timeout_kwargs(timeout)) - async def _register_external_client(self, params: MCPRegisterExternalClientRequest, *, timeout: float | None = None) -> None: "Registers a pre-connected external MCP client (e.g. IDE) on the session's host. The caller retains lifecycle ownership of the client and transport. Marked internal because the `client` and `transport` arguments are in-process MCP SDK instances that cannot be serialized across the JSON-RPC boundary; once the CLI moves on top of the SDK, external clients will be expressed as transport configs the runtime can construct itself.\n\nArgs:\n params: Registration parameters for an external MCP client.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} @@ -28270,6 +29242,12 @@ async def handle_canvas_action_invoke(params: dict) -> dict | None: return result.value if hasattr(result, 'value') else result client.set_request_handler("canvas.action.invoke", handle_canvas_action_invoke) +# Experimental: this API group is experimental and may change or be removed. +class HooksHandler(Protocol): + async def invoke(self, params: _HookInvokeRequest) -> _HookInvokeResponse: + "Dispatches one SDK callback hook from the runtime to the connection that registered it. Internal transport plumbing: clients opt in through session initialization and the Rust hook processor owns ordering, policy, timeout, and callback routing.\n\nArgs:\n params: Runtime-owned wire payload for a server-to-client hook callback invocation.\n\nReturns:\n Optional output returned by an SDK callback hook." + pass + # Experimental: this API group is experimental and may change or be removed. class LlmInferenceHandler(Protocol): async def http_request_start(self, params: LlmInferenceHTTPRequestStartRequest) -> LlmInferenceHTTPRequestStartResult: @@ -28287,6 +29265,7 @@ async def event(self, params: GitHubTelemetryNotification) -> None: @dataclass class ClientGlobalApiHandlers: + hooks: HooksHandler | None = None llm_inference: LlmInferenceHandler | None = None git_hub_telemetry: GitHubTelemetryHandler | None = None @@ -28300,6 +29279,13 @@ def register_client_global_api_handlers( session_id dispatch key; a single set of handlers serves the entire connection. """ + async def handle_hooks_invoke(params: dict) -> dict | None: + request = _HookInvokeRequest.from_dict(params) + handler = handlers.hooks + if handler is None: raise RuntimeError("No hooks client-global handler registered") + result = await handler.invoke(request) + return result.to_dict() + client.set_request_handler("hooks.invoke", handle_hooks_invoke) async def handle_llm_inference_http_request_start(params: dict) -> dict | None: request = LlmInferenceHTTPRequestStartRequest.from_dict(params) handler = handlers.llm_inference @@ -28518,6 +29504,7 @@ async def handle_git_hub_telemetry_event(params: dict) -> None: "HistorySummarizeForHandoffResult", "HistoryTruncateRequest", "HistoryTruncateResult", + "HooksHandler", "Host", "HostType", "InstalledPlugin", @@ -28609,6 +29596,17 @@ async def handle_git_hub_telemetry_event(params: dict) -> None: "MCPRegisterExternalClientRequest", "MCPReloadWithConfigRequest", "MCPRemoveGitHubResult", + "MCPResource", + "MCPResourceAnnotations", + "MCPResourceContent", + "MCPResourceIcon", + "MCPResourceTemplate", + "MCPResourcesListRequest", + "MCPResourcesListResult", + "MCPResourcesListTemplatesRequest", + "MCPResourcesListTemplatesResult", + "MCPResourcesReadRequest", + "MCPResourcesReadResult", "MCPRestartServerRequest", "MCPSamplingExecutionAction", "MCPSamplingExecutionResult", @@ -28628,6 +29626,8 @@ async def handle_git_hub_telemetry_event(params: dict) -> None: "MCPStartServerRequest", "MCPStartServersResult", "MCPStopServerRequest", + "MCPToolUI", + "MCPToolUIVisibility", "MCPTools", "MCPUnregisterExternalClientRequest", "MarketplaceAddResult", @@ -28652,6 +29652,7 @@ async def handle_git_hub_telemetry_event(params: dict) -> None: "McpHeadersApi", "McpOauthApi", "McpOauthLoginGrantType", + "McpResourcesApi", "McpServerAuthConfig", "McpServerConfigHttpOauthGrantType", "MemoryConfiguration", @@ -28677,6 +29678,7 @@ async def handle_git_hub_telemetry_event(params: dict) -> None: "Model", "ModelApi", "ModelBilling", + "ModelBillingPromo", "ModelBillingTokenPrices", "ModelBillingTokenPricesLongContext", "ModelCapabilities", @@ -28984,6 +29986,9 @@ async def handle_git_hub_telemetry_event(params: dict) -> None: "SecretsAddFilterValuesResult", "SendAgentMode", "SendAttachmentsToMessageParams", + "SendMessageItem", + "SendMessagesRequest", + "SendMessagesResult", "SendMode", "SendRequest", "SendResult", @@ -28991,6 +29996,7 @@ async def handle_git_hub_telemetry_event(params: dict) -> None: "ServerAgentList", "ServerAgentRegistryApi", "ServerAgentsApi", + "ServerCommandsApi", "ServerInstructionSourceList", "ServerInstructionsApi", "ServerLlmInferenceApi", @@ -29063,6 +30069,7 @@ async def handle_git_hub_telemetry_event(params: dict) -> None: "SessionMcpAppsCallToolResult", "SessionMetadataSnapshot", "SessionModelList", + "SessionModelPriceCategory", "SessionOpenOptions", "SessionOpenOptionsAdditionalContentExclusionPolicy", "SessionOpenOptionsAdditionalContentExclusionPolicyRule", diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index ceb9b764ba..a7c990e169 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -155,8 +155,10 @@ class SessionEventType(Enum): PENDING_MESSAGES_MODIFIED = "pending_messages.modified" ASSISTANT_TURN_START = "assistant.turn_start" ASSISTANT_INTENT = "assistant.intent" + ASSISTANT_SERVER_TOOL_PROGRESS = "assistant.server_tool_progress" ASSISTANT_REASONING = "assistant.reasoning" ASSISTANT_REASONING_DELTA = "assistant.reasoning_delta" + ASSISTANT_TOOL_CALL_DELTA = "assistant.tool_call_delta" ASSISTANT_STREAMING_DELTA = "assistant.streaming_delta" ASSISTANT_MESSAGE = "assistant.message" ASSISTANT_MESSAGE_START = "assistant.message_start" @@ -206,6 +208,10 @@ class SessionEventType(Enum): AUTO_MODE_SWITCH_COMPLETED = "auto_mode_switch.completed" SESSION_LIMITS_EXHAUSTED_REQUESTED = "session_limits_exhausted.requested" SESSION_LIMITS_EXHAUSTED_COMPLETED = "session_limits_exhausted.completed" + # Experimental: this event is part of an experimental API and may change or be removed. + SESSION_AUTO_MODE_RESOLVED = "session.auto_mode_resolved" + # Experimental: this event is part of an experimental API and may change or be removed. + SESSION_MANAGED_SETTINGS_RESOLVED = "session.managed_settings_resolved" COMMANDS_CHANGED = "commands.changed" CAPABILITIES_CHANGED = "capabilities.changed" EXIT_PLAN_MODE_REQUESTED = "exit_plan_mode.requested" @@ -216,6 +222,9 @@ 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" + MCP_TOOLS_LIST_CHANGED = "mcp.tools.list_changed" + MCP_RESOURCES_LIST_CHANGED = "mcp.resources.list_changed" + MCP_PROMPTS_LIST_CHANGED = "mcp.prompts.list_changed" SESSION_EXTENSIONS_LOADED = "session.extensions_loaded" # Experimental: this event is part of an experimental API and may change or be removed. SESSION_CANVAS_OPENED = "session.canvas.opened" @@ -445,6 +454,7 @@ class CanvasRegistryChangedCanvas: extension_id: str actions: list[CanvasRegistryChangedCanvasAction] | None = None extension_name: str | None = None + icon: str | None = None input_schema: Any = None @staticmethod @@ -456,6 +466,7 @@ def from_dict(obj: Any) -> "CanvasRegistryChangedCanvas": extension_id = from_str(obj.get("extensionId")) actions = from_union([from_none, lambda x: from_list(CanvasRegistryChangedCanvasAction.from_dict, x)], obj.get("actions")) extension_name = from_union([from_none, from_str], obj.get("extensionName")) + icon = from_union([from_none, from_str], obj.get("icon")) input_schema = obj.get("inputSchema") return CanvasRegistryChangedCanvas( canvas_id=canvas_id, @@ -464,6 +475,7 @@ def from_dict(obj: Any) -> "CanvasRegistryChangedCanvas": extension_id=extension_id, actions=actions, extension_name=extension_name, + icon=icon, input_schema=input_schema, ) @@ -477,6 +489,8 @@ def to_dict(self) -> dict: result["actions"] = from_union([from_none, lambda x: from_list(lambda x: to_class(CanvasRegistryChangedCanvasAction, x), x)], self.actions) if self.extension_name is not None: result["extensionName"] = from_union([from_none, from_str], self.extension_name) + if self.icon is not None: + result["icon"] = from_union([from_none, from_str], self.icon) if self.input_schema is not None: result["inputSchema"] = self.input_schema return result @@ -822,6 +836,51 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionAutoModeResolvedData: + "Auto Intent resolution: the concrete model the session settled on for the first prompt of an auto-mode session, and why. Lets SDK clients render the chosen model and the full reason it was picked. The core selection fields (chosenModel/reasoningBucket/categoryScores) are stable; the routing-analytics fields (predictedLabel/confidence/candidateModels) mirror the upstream intent service and may evolve, hence the event's experimental stability." + chosen_model: str + candidate_models: list[str] | None = None + category_scores: dict[str, float] | None = None + confidence: float | None = None + predicted_label: str | None = None + reasoning_bucket: AutoModeResolvedReasoningBucket | None = None + + @staticmethod + def from_dict(obj: Any) -> "SessionAutoModeResolvedData": + assert isinstance(obj, dict) + chosen_model = from_str(obj.get("chosenModel")) + candidate_models = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("candidateModels")) + category_scores = from_union([from_none, lambda x: from_dict(from_float, x)], obj.get("categoryScores")) + confidence = from_union([from_none, from_float], obj.get("confidence")) + predicted_label = from_union([from_none, from_str], obj.get("predictedLabel")) + reasoning_bucket = from_union([from_none, lambda x: parse_enum(AutoModeResolvedReasoningBucket, x)], obj.get("reasoningBucket")) + return SessionAutoModeResolvedData( + chosen_model=chosen_model, + candidate_models=candidate_models, + category_scores=category_scores, + confidence=confidence, + predicted_label=predicted_label, + reasoning_bucket=reasoning_bucket, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["chosenModel"] = from_str(self.chosen_model) + if self.candidate_models is not None: + result["candidateModels"] = from_union([from_none, lambda x: from_list(from_str, x)], self.candidate_models) + if self.category_scores is not None: + result["categoryScores"] = from_union([from_none, lambda x: from_dict(to_float, x)], self.category_scores) + if self.confidence is not None: + result["confidence"] = from_union([from_none, to_float], self.confidence) + if self.predicted_label is not None: + result["predictedLabel"] = from_union([from_none, from_str], self.predicted_label) + if self.reasoning_bucket is not None: + result["reasoningBucket"] = from_union([from_none, lambda x: to_enum(AutoModeResolvedReasoningBucket, x)], self.reasoning_bucket) + return result + + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionCanvasClosedData: @@ -853,11 +912,12 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionCanvasOpenedData: - "Payload of `session.canvas.opened` with canvas instance and provider IDs plus optional title, status, URL, and input." + "Payload of `session.canvas.opened` with canvas instance and provider IDs plus optional icon, title, status, URL, and input." canvas_id: str extension_id: str instance_id: str extension_name: str | None = None + icon: str | None = None input: Any = None status: str | None = None title: str | None = None @@ -870,6 +930,7 @@ def from_dict(obj: Any) -> "SessionCanvasOpenedData": extension_id = from_str(obj.get("extensionId")) instance_id = from_str(obj.get("instanceId")) extension_name = from_union([from_none, from_str], obj.get("extensionName")) + icon = from_union([from_none, from_str], obj.get("icon")) input = obj.get("input") status = from_union([from_none, from_str], obj.get("status")) title = from_union([from_none, from_str], obj.get("title")) @@ -879,6 +940,7 @@ def from_dict(obj: Any) -> "SessionCanvasOpenedData": extension_id=extension_id, instance_id=instance_id, extension_name=extension_name, + icon=icon, input=input, status=status, title=title, @@ -892,6 +954,8 @@ def to_dict(self) -> dict: result["instanceId"] = from_str(self.instance_id) if self.extension_name is not None: result["extensionName"] = from_union([from_none, from_str], self.extension_name) + if self.icon is not None: + result["icon"] = from_union([from_none, from_str], self.icon) if self.input is not None: result["input"] = self.input if self.status is not None: @@ -1017,6 +1081,51 @@ def to_dict(self) -> dict: return result +# 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 where they came from, so SDK clients can show users what is enterprise-managed and by which authority. 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; for a session-independent pull, use the SDK `getManagedSettings()` API, which returns the identical payload. Managed settings have a single authoritative source, so the highest-authority present layer (server > device) wins wholesale; `bypassPermissionsDisabled` is deny-wins across layers. Marked experimental while the managed-settings surface stabilizes." + bypass_permissions_disabled: bool + device_managed: bool + fail_closed: bool + managed_keys: list[str] + server_managed: bool + source: ManagedSettingsResolvedSource + settings: Any = None + + @staticmethod + def from_dict(obj: Any) -> "SessionManagedSettingsResolvedData": + assert isinstance(obj, dict) + bypass_permissions_disabled = from_bool(obj.get("bypassPermissionsDisabled")) + device_managed = from_bool(obj.get("deviceManaged")) + fail_closed = from_bool(obj.get("failClosed")) + managed_keys = from_list(from_str, obj.get("managedKeys")) + server_managed = from_bool(obj.get("serverManaged")) + source = parse_enum(ManagedSettingsResolvedSource, obj.get("source")) + settings = obj.get("settings") + return SessionManagedSettingsResolvedData( + bypass_permissions_disabled=bypass_permissions_disabled, + device_managed=device_managed, + fail_closed=fail_closed, + managed_keys=managed_keys, + server_managed=server_managed, + source=source, + settings=settings, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["bypassPermissionsDisabled"] = from_bool(self.bypass_permissions_disabled) + result["deviceManaged"] = from_bool(self.device_managed) + result["failClosed"] = from_bool(self.fail_closed) + result["managedKeys"] = from_list(from_str, self.managed_keys) + result["serverManaged"] = from_bool(self.server_managed) + result["source"] = to_enum(ManagedSettingsResolvedSource, self.source) + if self.settings is not None: + result["settings"] = self.settings + return result + + @dataclass class AbortData: "Turn abort information including the reason for termination" @@ -1083,6 +1192,7 @@ class AssistantMessageData: api_call_id: str | None = None # Experimental: this field is part of an experimental API and may change or be removed. citations: Citations | None = None + client_request_id: str | None = None encrypted_content: str | None = None interaction_id: str | None = None model: str | None = None @@ -1106,6 +1216,7 @@ def from_dict(obj: Any) -> "AssistantMessageData": message_id = from_str(obj.get("messageId")) api_call_id = from_union([from_none, from_str], obj.get("apiCallId")) citations = from_union([from_none, Citations.from_dict], obj.get("citations")) + client_request_id = from_union([from_none, from_str], obj.get("clientRequestId")) encrypted_content = from_union([from_none, from_str], obj.get("encryptedContent")) interaction_id = from_union([from_none, from_str], obj.get("interactionId")) model = from_union([from_none, from_str], obj.get("model")) @@ -1125,6 +1236,7 @@ def from_dict(obj: Any) -> "AssistantMessageData": message_id=message_id, api_call_id=api_call_id, citations=citations, + client_request_id=client_request_id, encrypted_content=encrypted_content, interaction_id=interaction_id, model=model, @@ -1149,6 +1261,8 @@ def to_dict(self) -> dict: result["apiCallId"] = from_union([from_none, from_str], self.api_call_id) if self.citations is not None: result["citations"] = from_union([from_none, lambda x: to_class(Citations, x)], self.citations) + if self.client_request_id is not None: + result["clientRequestId"] = from_union([from_none, from_str], self.client_request_id) if self.encrypted_content is not None: result["encryptedContent"] = from_union([from_none, from_str], self.encrypted_content) if self.interaction_id is not None: @@ -1332,6 +1446,33 @@ def to_dict(self) -> dict: return result +@dataclass +class AssistantServerToolProgressData: + "Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message" + kind: str + output_index: int + status: str + + @staticmethod + def from_dict(obj: Any) -> "AssistantServerToolProgressData": + assert isinstance(obj, dict) + kind = from_str(obj.get("kind")) + output_index = from_int(obj.get("outputIndex")) + status = from_str(obj.get("status")) + return AssistantServerToolProgressData( + kind=kind, + output_index=output_index, + status=status, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = from_str(self.kind) + result["outputIndex"] = to_int(self.output_index) + result["status"] = from_str(self.status) + return result + + @dataclass class AssistantStreamingDeltaData: "Streaming response progress with cumulative byte count" @@ -1351,6 +1492,39 @@ def to_dict(self) -> dict: return result +@dataclass +class AssistantToolCallDeltaData: + "Streaming tool-call input delta for incremental tool-call updates" + input_delta: str + tool_call_id: str + tool_name: str | None = None + tool_type: AssistantMessageToolRequestType | None = None + + @staticmethod + def from_dict(obj: Any) -> "AssistantToolCallDeltaData": + assert isinstance(obj, dict) + input_delta = from_str(obj.get("inputDelta")) + tool_call_id = from_str(obj.get("toolCallId")) + tool_name = from_union([from_none, from_str], obj.get("toolName")) + tool_type = from_union([from_none, lambda x: parse_enum(AssistantMessageToolRequestType, x)], obj.get("toolType")) + return AssistantToolCallDeltaData( + input_delta=input_delta, + tool_call_id=tool_call_id, + tool_name=tool_name, + tool_type=tool_type, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["inputDelta"] = from_str(self.input_delta) + result["toolCallId"] = from_str(self.tool_call_id) + if self.tool_name is not None: + result["toolName"] = from_union([from_none, from_str], self.tool_name) + if self.tool_type is not None: + result["toolType"] = from_union([from_none, lambda x: to_enum(AssistantMessageToolRequestType, x)], self.tool_type) + return result + + @dataclass class AssistantTurnEndData: "Turn completion metadata including the turn identifier" @@ -3102,6 +3276,29 @@ def to_dict(self) -> dict: return result +@dataclass +class HeaderEntry: + "Single HTTP header entry as a name/value pair." + name: str + value: str + + @staticmethod + def from_dict(obj: Any) -> "HeaderEntry": + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + value = from_str(obj.get("value")) + return HeaderEntry( + name=name, + value=value, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + result["value"] = from_str(self.value) + return result + + @dataclass class HookEndData: "Hook invocation completion details including output, success status, and error information" @@ -3412,6 +3609,34 @@ def to_dict(self) -> dict: return result +@dataclass +class McpOauthHttpResponse: + "Raw HTTP response details from the OAuth auth challenge, as observed by the runtime." + headers: list[HeaderEntry] + status_code: int + body: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "McpOauthHttpResponse": + assert isinstance(obj, dict) + headers = from_list(HeaderEntry.from_dict, obj.get("headers")) + status_code = from_int(obj.get("statusCode")) + body = from_union([from_none, from_str], obj.get("body")) + return McpOauthHttpResponse( + headers=headers, + status_code=status_code, + body=body, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["headers"] = from_list(lambda x: to_class(HeaderEntry, x), self.headers) + result["statusCode"] = to_int(self.status_code) + if self.body is not None: + result["body"] = from_union([from_none, from_str], self.body) + return result + + @dataclass class McpOauthRequiredData: "OAuth authentication request for an MCP server" @@ -3419,6 +3644,7 @@ class McpOauthRequiredData: request_id: str server_name: str server_url: str + http_response: McpOauthHttpResponse | None = None resource_metadata: str | None = None static_client_config: McpOauthRequiredStaticClientConfig | None = None www_authenticate_params: McpOauthWWWAuthenticateParams | None = None @@ -3430,6 +3656,7 @@ def from_dict(obj: Any) -> "McpOauthRequiredData": request_id = from_str(obj.get("requestId")) server_name = from_str(obj.get("serverName")) server_url = from_str(obj.get("serverUrl")) + http_response = from_union([from_none, McpOauthHttpResponse.from_dict], obj.get("httpResponse")) resource_metadata = from_union([from_none, from_str], obj.get("resourceMetadata")) static_client_config = from_union([from_none, McpOauthRequiredStaticClientConfig.from_dict], obj.get("staticClientConfig")) www_authenticate_params = from_union([from_none, McpOauthWWWAuthenticateParams.from_dict], obj.get("wwwAuthenticateParams")) @@ -3438,6 +3665,7 @@ def from_dict(obj: Any) -> "McpOauthRequiredData": request_id=request_id, server_name=server_name, server_url=server_url, + http_response=http_response, resource_metadata=resource_metadata, static_client_config=static_client_config, www_authenticate_params=www_authenticate_params, @@ -3449,6 +3677,8 @@ def to_dict(self) -> dict: result["requestId"] = from_str(self.request_id) result["serverName"] = from_str(self.server_name) result["serverUrl"] = from_str(self.server_url) + if self.http_response is not None: + result["httpResponse"] = from_union([from_none, lambda x: to_class(McpOauthHttpResponse, x)], self.http_response) if self.resource_metadata is not None: result["resourceMetadata"] = from_union([from_none, from_str], self.resource_metadata) if self.static_client_config is not None: @@ -3522,6 +3752,44 @@ def to_dict(self) -> dict: return result +@dataclass +class McpPromptsListChangedData: + "Payload identifying the MCP server associated with a list change." + server_name: str + + @staticmethod + def from_dict(obj: Any) -> "McpPromptsListChangedData": + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + return McpPromptsListChangedData( + server_name=server_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["serverName"] = from_str(self.server_name) + return result + + +@dataclass +class McpResourcesListChangedData: + "Payload identifying the MCP server associated with a list change." + server_name: str + + @staticmethod + def from_dict(obj: Any) -> "McpResourcesListChangedData": + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + return McpResourcesListChangedData( + server_name=server_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["serverName"] = from_str(self.server_name) + return result + + @dataclass class McpServersLoadedServer: "A single MCP server status summary in `session.mcp_servers_loaded`, including name, status, source, transport, and plugin metadata." @@ -3570,6 +3838,25 @@ def to_dict(self) -> dict: return result +@dataclass +class McpToolsListChangedData: + "Payload identifying the MCP server associated with a list change." + server_name: str + + @staticmethod + def from_dict(obj: Any) -> "McpToolsListChangedData": + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + return McpToolsListChangedData( + server_name=server_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["serverName"] = from_str(self.server_name) + return result + + @dataclass class ModelCallFailureData: "Failed LLM API call metadata for telemetry" @@ -4328,6 +4615,8 @@ class PermissionPromptRequestUrl: url: str # Experimental: this field is part of an experimental API and may change or be removed. auto_approval: PermissionAutoApproval | None = None + request_sandbox_bypass: bool | None = None + request_sandbox_bypass_reason: str | None = None tool_call_id: str | None = None @staticmethod @@ -4336,11 +4625,15 @@ def from_dict(obj: Any) -> "PermissionPromptRequestUrl": intention = from_str(obj.get("intention")) url = from_str(obj.get("url")) auto_approval = from_union([from_none, PermissionAutoApproval.from_dict], obj.get("autoApproval")) + request_sandbox_bypass = from_union([from_none, from_bool], obj.get("requestSandboxBypass")) + request_sandbox_bypass_reason = from_union([from_none, from_str], obj.get("requestSandboxBypassReason")) tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) return PermissionPromptRequestUrl( intention=intention, url=url, auto_approval=auto_approval, + request_sandbox_bypass=request_sandbox_bypass, + request_sandbox_bypass_reason=request_sandbox_bypass_reason, tool_call_id=tool_call_id, ) @@ -4351,6 +4644,10 @@ def to_dict(self) -> dict: result["url"] = from_str(self.url) if self.auto_approval is not None: result["autoApproval"] = from_union([from_none, lambda x: to_class(PermissionAutoApproval, x)], self.auto_approval) + if self.request_sandbox_bypass is not None: + result["requestSandboxBypass"] = from_union([from_none, from_bool], self.request_sandbox_bypass) + if self.request_sandbox_bypass_reason is not None: + result["requestSandboxBypassReason"] = from_union([from_none, from_str], self.request_sandbox_bypass_reason) if self.tool_call_id is not None: result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) return result @@ -4784,6 +5081,8 @@ class PermissionRequestUrl: intention: str kind: ClassVar[str] = "url" url: str + request_sandbox_bypass: bool | None = None + request_sandbox_bypass_reason: str | None = None tool_call_id: str | None = None @staticmethod @@ -4791,10 +5090,14 @@ def from_dict(obj: Any) -> "PermissionRequestUrl": assert isinstance(obj, dict) intention = from_str(obj.get("intention")) url = from_str(obj.get("url")) + request_sandbox_bypass = from_union([from_none, from_bool], obj.get("requestSandboxBypass")) + request_sandbox_bypass_reason = from_union([from_none, from_str], obj.get("requestSandboxBypassReason")) tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) return PermissionRequestUrl( intention=intention, url=url, + request_sandbox_bypass=request_sandbox_bypass, + request_sandbox_bypass_reason=request_sandbox_bypass_reason, tool_call_id=tool_call_id, ) @@ -4803,6 +5106,10 @@ def to_dict(self) -> dict: result["intention"] = from_str(self.intention) result["kind"] = self.kind result["url"] = from_str(self.url) + if self.request_sandbox_bypass is not None: + result["requestSandboxBypass"] = from_union([from_none, from_bool], self.request_sandbox_bypass) + if self.request_sandbox_bypass_reason is not None: + result["requestSandboxBypassReason"] = from_union([from_none, from_str], self.request_sandbox_bypass_reason) if self.tool_call_id is not None: result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) return result @@ -4817,6 +5124,8 @@ class PermissionRequestWrite: intention: str kind: ClassVar[str] = "write" new_file_contents: str | None = None + request_sandbox_bypass: bool | None = None + request_sandbox_bypass_reason: str | None = None tool_call_id: str | None = None @staticmethod @@ -4827,6 +5136,8 @@ def from_dict(obj: Any) -> "PermissionRequestWrite": file_name = from_str(obj.get("fileName")) intention = from_str(obj.get("intention")) new_file_contents = from_union([from_none, from_str], obj.get("newFileContents")) + request_sandbox_bypass = from_union([from_none, from_bool], obj.get("requestSandboxBypass")) + request_sandbox_bypass_reason = from_union([from_none, from_str], obj.get("requestSandboxBypassReason")) tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) return PermissionRequestWrite( can_offer_session_approval=can_offer_session_approval, @@ -4834,6 +5145,8 @@ def from_dict(obj: Any) -> "PermissionRequestWrite": file_name=file_name, intention=intention, new_file_contents=new_file_contents, + request_sandbox_bypass=request_sandbox_bypass, + request_sandbox_bypass_reason=request_sandbox_bypass_reason, tool_call_id=tool_call_id, ) @@ -4846,6 +5159,10 @@ def to_dict(self) -> dict: result["kind"] = self.kind if self.new_file_contents is not None: result["newFileContents"] = from_union([from_none, from_str], self.new_file_contents) + if self.request_sandbox_bypass is not None: + result["requestSandboxBypass"] = from_union([from_none, from_bool], self.request_sandbox_bypass) + if self.request_sandbox_bypass_reason is not None: + result["requestSandboxBypassReason"] = from_union([from_none, from_str], self.request_sandbox_bypass_reason) if self.tool_call_id is not None: result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) return result @@ -5708,8 +6025,10 @@ class SessionModelChangeData: previous_model: str | None = None previous_reasoning_effort: str | None = None previous_reasoning_summary: ReasoningSummary | None = None + previous_verbosity: Verbosity | None = None reasoning_effort: str | None = None reasoning_summary: ReasoningSummary | None = None + verbosity: Verbosity | None = None @staticmethod def from_dict(obj: Any) -> "SessionModelChangeData": @@ -5720,8 +6039,10 @@ def from_dict(obj: Any) -> "SessionModelChangeData": 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")) + previous_verbosity = from_union([from_none, lambda x: parse_enum(Verbosity, x)], obj.get("previousVerbosity")) reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) reasoning_summary = from_union([from_none, lambda x: parse_enum(ReasoningSummary, x)], obj.get("reasoningSummary")) + verbosity = from_union([from_none, lambda x: parse_enum(Verbosity, x)], obj.get("verbosity")) return SessionModelChangeData( new_model=new_model, cause=cause, @@ -5729,8 +6050,10 @@ def from_dict(obj: Any) -> "SessionModelChangeData": previous_model=previous_model, previous_reasoning_effort=previous_reasoning_effort, previous_reasoning_summary=previous_reasoning_summary, + previous_verbosity=previous_verbosity, reasoning_effort=reasoning_effort, reasoning_summary=reasoning_summary, + verbosity=verbosity, ) def to_dict(self) -> dict: @@ -5746,10 +6069,14 @@ def to_dict(self) -> dict: result["previousReasoningEffort"] = from_union([from_none, from_str], self.previous_reasoning_effort) if self.previous_reasoning_summary is not None: result["previousReasoningSummary"] = from_union([from_none, lambda x: to_enum(ReasoningSummary, x)], self.previous_reasoning_summary) + if self.previous_verbosity is not None: + result["previousVerbosity"] = from_union([from_none, lambda x: to_enum(Verbosity, x)], self.previous_verbosity) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_none, from_str], self.reasoning_effort) if self.reasoning_summary is not None: result["reasoningSummary"] = from_union([from_none, lambda x: to_enum(ReasoningSummary, x)], self.reasoning_summary) + if self.verbosity is not None: + result["verbosity"] = from_union([from_none, lambda x: to_enum(Verbosity, x)], self.verbosity) return result @@ -5842,6 +6169,7 @@ class SessionResumeData: selected_model: str | None = None session_limits: SessionLimitsConfig | None = None session_was_active: bool | None = None + verbosity: Verbosity | None = None @staticmethod def from_dict(obj: Any) -> "SessionResumeData": @@ -5859,6 +6187,7 @@ def from_dict(obj: Any) -> "SessionResumeData": selected_model = from_union([from_none, from_str], obj.get("selectedModel")) session_limits = from_union([from_none, SessionLimitsConfig.from_dict], obj.get("sessionLimits")) session_was_active = from_union([from_none, from_bool], obj.get("sessionWasActive")) + verbosity = from_union([from_none, lambda x: parse_enum(Verbosity, x)], obj.get("verbosity")) return SessionResumeData( event_count=event_count, resume_time=resume_time, @@ -5873,6 +6202,7 @@ def from_dict(obj: Any) -> "SessionResumeData": selected_model=selected_model, session_limits=session_limits, session_was_active=session_was_active, + verbosity=verbosity, ) def to_dict(self) -> dict: @@ -5901,6 +6231,8 @@ def to_dict(self) -> dict: result["sessionLimits"] = from_union([from_none, lambda x: to_class(SessionLimitsConfig, x)], self.session_limits) if self.session_was_active is not None: result["sessionWasActive"] = from_union([from_none, from_bool], self.session_was_active) + if self.verbosity is not None: + result["verbosity"] = from_union([from_none, lambda x: to_enum(Verbosity, x)], self.verbosity) return result @@ -6169,6 +6501,7 @@ class SessionStartData: remote_steerable: bool | None = None selected_model: str | None = None session_limits: SessionLimitsConfig | None = None + verbosity: Verbosity | None = None @staticmethod def from_dict(obj: Any) -> "SessionStartData": @@ -6187,6 +6520,7 @@ def from_dict(obj: Any) -> "SessionStartData": remote_steerable = from_union([from_none, from_bool], obj.get("remoteSteerable")) selected_model = from_union([from_none, from_str], obj.get("selectedModel")) session_limits = from_union([from_none, SessionLimitsConfig.from_dict], obj.get("sessionLimits")) + verbosity = from_union([from_none, lambda x: parse_enum(Verbosity, x)], obj.get("verbosity")) return SessionStartData( copilot_version=copilot_version, producer=producer, @@ -6202,6 +6536,7 @@ def from_dict(obj: Any) -> "SessionStartData": remote_steerable=remote_steerable, selected_model=selected_model, session_limits=session_limits, + verbosity=verbosity, ) def to_dict(self) -> dict: @@ -6229,6 +6564,8 @@ def to_dict(self) -> dict: result["selectedModel"] = from_union([from_none, from_str], self.selected_model) if self.session_limits is not None: result["sessionLimits"] = from_union([from_none, lambda x: to_class(SessionLimitsConfig, x)], self.session_limits) + if self.verbosity is not None: + result["verbosity"] = from_union([from_none, lambda x: to_enum(Verbosity, x)], self.verbosity) return result @@ -7417,6 +7754,8 @@ class ToolExecutionCompleteData: error: ToolExecutionCompleteError | None = None interaction_id: str | None = None is_user_requested: bool | None = None + # Experimental: this field is part of an experimental API and may change or be removed. + mcp_meta: Any = None model: str | None = None # Deprecated: this field is deprecated. parent_tool_call_id: str | None = None @@ -7434,6 +7773,7 @@ def from_dict(obj: Any) -> "ToolExecutionCompleteData": error = from_union([from_none, ToolExecutionCompleteError.from_dict], obj.get("error")) interaction_id = from_union([from_none, from_str], obj.get("interactionId")) is_user_requested = from_union([from_none, from_bool], obj.get("isUserRequested")) + mcp_meta = obj.get("mcpMeta") model = from_union([from_none, from_str], obj.get("model")) parent_tool_call_id = from_union([from_none, from_str], obj.get("parentToolCallId")) result = from_union([from_none, ToolExecutionCompleteResult.from_dict], obj.get("result")) @@ -7447,6 +7787,7 @@ def from_dict(obj: Any) -> "ToolExecutionCompleteData": error=error, interaction_id=interaction_id, is_user_requested=is_user_requested, + mcp_meta=mcp_meta, model=model, parent_tool_call_id=parent_tool_call_id, result=result, @@ -7466,6 +7807,8 @@ def to_dict(self) -> dict: result["interactionId"] = from_union([from_none, from_str], self.interaction_id) if self.is_user_requested is not None: result["isUserRequested"] = from_union([from_none, from_bool], self.is_user_requested) + if self.mcp_meta is not None: + result["mcpMeta"] = self.mcp_meta if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) if self.parent_tool_call_id is not None: @@ -7517,6 +7860,8 @@ class ToolExecutionCompleteResult: citable_sources: list[CitableSource] | None = None contents: list[ToolExecutionCompleteContent] | None = None detailed_content: str | None = None + # Experimental: this field is part of an experimental API and may change or be removed. + mcp_meta: Any = None structured_content: Any = None ui_resource: ToolExecutionCompleteUIResource | None = None @@ -7528,6 +7873,7 @@ def from_dict(obj: Any) -> "ToolExecutionCompleteResult": citable_sources = from_union([from_none, lambda x: from_list(CitableSource.from_dict, x)], obj.get("citableSources")) contents = from_union([from_none, lambda x: from_list(_load_ToolExecutionCompleteContent, x)], obj.get("contents")) detailed_content = from_union([from_none, from_str], obj.get("detailedContent")) + mcp_meta = obj.get("mcpMeta") structured_content = obj.get("structuredContent") ui_resource = from_union([from_none, ToolExecutionCompleteUIResource.from_dict], obj.get("uiResource")) return ToolExecutionCompleteResult( @@ -7536,6 +7882,7 @@ def from_dict(obj: Any) -> "ToolExecutionCompleteResult": citable_sources=citable_sources, contents=contents, detailed_content=detailed_content, + mcp_meta=mcp_meta, structured_content=structured_content, ui_resource=ui_resource, ) @@ -7551,6 +7898,8 @@ def to_dict(self) -> dict: result["contents"] = from_union([from_none, lambda x: from_list(lambda x: x.to_dict(), x)], self.contents) if self.detailed_content is not None: result["detailedContent"] = from_union([from_none, from_str], self.detailed_content) + if self.mcp_meta is not None: + result["mcpMeta"] = self.mcp_meta if self.structured_content is not None: result["structuredContent"] = self.structured_content if self.ui_resource is not None: @@ -8674,6 +9023,16 @@ class AttachmentGitHubReferenceType(Enum): DISCUSSION = "discussion" +class AutoModeResolvedReasoningBucket(Enum): + "Coarse request-difficulty bucket for UX explainability" + # The request looks low-reasoning; a lighter model is appropriate. + LOW = "low" + # The request needs a moderate amount of reasoning. + MEDIUM = "medium" + # The request looks high-reasoning; a stronger model is appropriate. + HIGH = "high" + + class AutoModeSwitchResponse(Enum): "The user's auto-mode-switch choice" # Switch models for this request. @@ -8792,6 +9151,16 @@ class HandoffSourceType(Enum): LOCAL = "local" +class ManagedSettingsResolvedSource(Enum): + "Which channel supplied the effective enterprise managed settings (highest-authority present layer wins wholesale)" + # Account/org policy self-fetched from the GitHub managed-settings endpoint (higher authority). + SERVER = "server" + # Device-level MDM policy discovered from plist/registry/file (lower authority). + DEVICE = "device" + # No managed policy is in force (no layer contributed). + NONE = "none" + + class McpHeadersRefreshCompletedOutcome(Enum): "How the pending MCP headers refresh request resolved." # The host supplied dynamic headers. @@ -9080,6 +9449,16 @@ class UserMessageDelivery(Enum): QUEUED = "queued" +class Verbosity(Enum): + "Output verbosity level used for supported model calls (e.g. \"low\", \"medium\", \"high\")" + # A terse response was requested. + LOW = "low" + # A medium amount of response detail was requested. + MEDIUM = "medium" + # A more detailed response was requested. + HIGH = "high" + + class WorkingDirectoryContextHostType(Enum): "Hosting platform type of the repository (github or ado)" # Repository is hosted on GitHub. @@ -9096,7 +9475,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 | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantIntentData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | ModelCallFailureData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | SkillInvokedData | SubagentStartedData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | 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 | SessionModeChangedData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantIntentData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | ModelCallFailureData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | SkillInvokedData | SubagentStartedData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | SessionCanvasClosedData | SessionCanvasUnavailableData | SessionCanvasRecordedData | SessionCanvasRemovedData | SessionExtensionsAttachmentsPushedData | McpAppToolCallCompleteData | RawSessionEventData | Data @dataclass @@ -9155,8 +9534,10 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.PENDING_MESSAGES_MODIFIED: data = PendingMessagesModifiedData.from_dict(data_obj) case SessionEventType.ASSISTANT_TURN_START: data = AssistantTurnStartData.from_dict(data_obj) case SessionEventType.ASSISTANT_INTENT: data = AssistantIntentData.from_dict(data_obj) + case SessionEventType.ASSISTANT_SERVER_TOOL_PROGRESS: data = AssistantServerToolProgressData.from_dict(data_obj) case SessionEventType.ASSISTANT_REASONING: data = AssistantReasoningData.from_dict(data_obj) case SessionEventType.ASSISTANT_REASONING_DELTA: data = AssistantReasoningDeltaData.from_dict(data_obj) + case SessionEventType.ASSISTANT_TOOL_CALL_DELTA: data = AssistantToolCallDeltaData.from_dict(data_obj) case SessionEventType.ASSISTANT_STREAMING_DELTA: data = AssistantStreamingDeltaData.from_dict(data_obj) case SessionEventType.ASSISTANT_MESSAGE: data = AssistantMessageData.from_dict(data_obj) case SessionEventType.ASSISTANT_MESSAGE_START: data = AssistantMessageStartData.from_dict(data_obj) @@ -9205,6 +9586,8 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.AUTO_MODE_SWITCH_COMPLETED: data = AutoModeSwitchCompletedData.from_dict(data_obj) case SessionEventType.SESSION_LIMITS_EXHAUSTED_REQUESTED: data = SessionLimitsExhaustedRequestedData.from_dict(data_obj) case SessionEventType.SESSION_LIMITS_EXHAUSTED_COMPLETED: data = SessionLimitsExhaustedCompletedData.from_dict(data_obj) + case SessionEventType.SESSION_AUTO_MODE_RESOLVED: data = SessionAutoModeResolvedData.from_dict(data_obj) + case SessionEventType.SESSION_MANAGED_SETTINGS_RESOLVED: data = SessionManagedSettingsResolvedData.from_dict(data_obj) case SessionEventType.COMMANDS_CHANGED: data = CommandsChangedData.from_dict(data_obj) case SessionEventType.CAPABILITIES_CHANGED: data = CapabilitiesChangedData.from_dict(data_obj) case SessionEventType.EXIT_PLAN_MODE_REQUESTED: data = ExitPlanModeRequestedData.from_dict(data_obj) @@ -9215,6 +9598,9 @@ 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.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) case SessionEventType.SESSION_EXTENSIONS_LOADED: data = SessionExtensionsLoadedData.from_dict(data_obj) case SessionEventType.SESSION_CANVAS_OPENED: data = SessionCanvasOpenedData.from_dict(data_obj) case SessionEventType.SESSION_CANVAS_REGISTRY_CHANGED: data = SessionCanvasRegistryChangedData.from_dict(data_obj) @@ -9270,7 +9656,9 @@ def session_event_to_dict(x: SessionEvent) -> Any: "AssistantMessageToolRequestType", "AssistantReasoningData", "AssistantReasoningDeltaData", + "AssistantServerToolProgressData", "AssistantStreamingDeltaData", + "AssistantToolCallDeltaData", "AssistantTurnEndData", "AssistantTurnStartData", "AssistantUsageApiEndpoint", @@ -9301,6 +9689,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "AttachmentSelectionDetailsEnd", "AttachmentSelectionDetailsStart", "AutoApprovalRecommendation", + "AutoModeResolvedReasoningBucket", "AutoModeSwitchCompletedData", "AutoModeSwitchRequestedData", "AutoModeSwitchResponse", @@ -9351,10 +9740,12 @@ def session_event_to_dict(x: SessionEvent) -> Any: "GitHubRepoRef", "HandoffRepository", "HandoffSourceType", + "HeaderEntry", "HookEndData", "HookEndError", "HookProgressData", "HookStartData", + "ManagedSettingsResolvedSource", "McpAppToolCallCompleteData", "McpAppToolCallCompleteError", "McpAppToolCallCompleteToolMeta", @@ -9365,14 +9756,18 @@ def session_event_to_dict(x: SessionEvent) -> Any: "McpHeadersRefreshRequiredReason", "McpOauthCompletedData", "McpOauthCompletionOutcome", + "McpOauthHttpResponse", "McpOauthRequestReason", "McpOauthRequiredData", "McpOauthRequiredStaticClientConfig", "McpOauthWWWAuthenticateParams", + "McpPromptsListChangedData", + "McpResourcesListChangedData", "McpServerSource", "McpServerStatus", "McpServerTransport", "McpServersLoadedServer", + "McpToolsListChangedData", "ModelCallFailureBadRequestKind", "ModelCallFailureData", "ModelCallFailureRequestFingerprint", @@ -9432,6 +9827,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "ReasoningSummary", "SamplingCompletedData", "SamplingRequestedData", + "SessionAutoModeResolvedData", "SessionAutopilotObjectiveChangedData", "SessionBackgroundTasksChangedData", "SessionBinaryAssetData", @@ -9460,6 +9856,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "SessionLimitsExhaustedRequestedData", "SessionLimitsExhaustedResponse", "SessionLimitsExhaustedResponseAction", + "SessionManagedSettingsResolvedData", "SessionMcpServerStatusChangedData", "SessionMcpServersLoadedData", "SessionMode", @@ -9564,6 +9961,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "UserToolSessionApprovalMemory", "UserToolSessionApprovalRead", "UserToolSessionApprovalWrite", + "Verbosity", "WorkingDirectoryContext", "WorkingDirectoryContextHostType", "WorkspaceFileChangedOperation", diff --git a/python/copilot/session.py b/python/copilot/session.py index 50de96f191..b6736939ac 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -87,6 +87,11 @@ logger = logging.getLogger(__name__) +# Fixed name of the runtime's built-in tool-search tool. A client can replace +# its behavior by registering a tool with this exact name and +# ``overrides_built_in_tool=True``. +_TOOL_SEARCH_TOOL_NAME = "tool_search_tool" + if TYPE_CHECKING: from .session_fs_provider import SessionFsProvider @@ -1075,6 +1080,9 @@ class CustomAgentConfig(TypedDict, total=False): skills: NotRequired[list[str]] # Model identifier (e.g. "claude-haiku-4.5"); runtime falls back to parent model if unavailable model: NotRequired[str] + # Reasoning effort for this agent's model. When omitted, no per-agent override + # is sent and the backend chooses its default; the parent effort is not inherited. + reasoning_effort: NotRequired[ReasoningEffort] class DefaultAgentConfig(TypedDict, total=False): @@ -1134,6 +1142,28 @@ class LargeToolOutputConfig(TypedDict, total=False): output_directory: str +class ToolSearchConfig(TypedDict, total=False): + """ + Override for the runtime's built-in tool-search behavior. + + Tool search lets the model discover tools on demand instead of loading every + tool definition up front. When the total tool count exceeds the deferral + threshold, MCP and external tools are marked as deferred and surfaced through + the built-in ``tool_search_tool``. + + To override the tool-search tool's implementation, register a :class:`Tool` + named ``tool_search_tool`` with ``overrides_built_in_tool=True``. To customize + the in-prompt tool-search guidance, use the ``tool_instructions`` section of + the system message in ``"customize"`` mode. + """ + + # Toggle that enables or disables tool search. + enabled: bool + # Overrides the total tool count at which MCP and external tools are + # automatically deferred behind tool search. + defer_threshold: int + + class MemoryConfiguration(TypedDict): """ Configuration for session memory. @@ -1932,11 +1962,25 @@ async def _execute_tool_and_respond( ) -> None: """Execute a tool handler and send the result back via HandlePendingToolCall RPC.""" try: + # The built-in tool-search tool receives a snapshot of the session's + # currently initialized tools so an override can filter the live + # catalog without issuing its own RPC. Fetch it only for that tool to + # avoid a round-trip on every tool call; a failed fetch leaves the + # snapshot as None rather than failing the tool. + available_tools = None + if tool_name == _TOOL_SEARCH_TOOL_NAME: + try: + metadata = await self.rpc.tools.get_current_metadata() + available_tools = metadata.tools + except Exception: + available_tools = None + invocation = ToolInvocation( session_id=self.session_id, tool_call_id=tool_call_id, tool_name=tool_name, arguments=arguments, + available_tools=available_tools, ) with trace_context(traceparent, tracestate): @@ -1997,6 +2041,7 @@ async def _execute_tool_and_respond( text_result_for_llm=tool_result.text_result_for_llm, error=tool_result.error, result_type=tool_result.result_type, + tool_references=tool_result.tool_references, tool_telemetry=tool_result.tool_telemetry, ), ) @@ -2850,7 +2895,7 @@ async def set_model( is preserved. Args: - model: Model ID to switch to (e.g., "gpt-4.1", "claude-sonnet-4"). + model: Model ID to switch to (e.g., "gpt-5.4", "claude-sonnet-4"). reasoning_effort: Optional reasoning effort level for the new model (e.g., "low", "medium", "high", "xhigh"). reasoning_summary: Optional reasoning summary mode for supported @@ -2864,7 +2909,7 @@ async def set_model( Exception: If the session has been destroyed or the connection fails. Example: - >>> await session.set_model("gpt-4.1") + >>> await session.set_model("gpt-5.4") >>> await session.set_model("claude-sonnet-4.6", reasoning_effort="high") """ rpc_caps = None diff --git a/python/copilot/tools.py b/python/copilot/tools.py index a82a48b1e9..b96e303e3b 100644 --- a/python/copilot/tools.py +++ b/python/copilot/tools.py @@ -11,9 +11,12 @@ import json from collections.abc import Awaitable, Callable from dataclasses import dataclass, field -from typing import Any, Literal, TypeVar, get_type_hints, overload +from typing import TYPE_CHECKING, Any, Literal, TypeVar, get_type_hints, overload -from pydantic import BaseModel +from pydantic import BaseModel, ValidationError + +if TYPE_CHECKING: + from .generated.rpc import CurrentToolMetadata ToolResultType = Literal["success", "failure", "rejected", "denied", "timeout"] @@ -38,6 +41,7 @@ class ToolResult: binary_results_for_llm: list[ToolBinaryResult] | None = None session_log: str | None = None tool_telemetry: dict[str, Any] | None = None + tool_references: list[str] | None = None _from_exception: bool = field(default=False, repr=False) @@ -49,6 +53,14 @@ class ToolInvocation: tool_call_id: str = "" tool_name: str = "" arguments: Any = None + available_tools: list[CurrentToolMetadata] | None = None + """Snapshot of the session's currently initialized tools. + + Populated by the SDK only when this invocation targets the built-in + tool-search tool (``tool_search_tool``), so a tool-search override can + rank/filter the live catalog -- including MCP tools configured in settings -- + without issuing its own RPC. ``None`` for every other tool invocation. + """ ToolHandler = Callable[[ToolInvocation], ToolResult | Awaitable[ToolResult]] @@ -63,6 +75,7 @@ class Tool: overrides_built_in_tool: bool = False skip_permission: bool = False defer: Literal["auto", "never"] | None = None + metadata: dict[str, Any] | None = None T = TypeVar("T", bound=BaseModel) @@ -77,6 +90,7 @@ def define_tool( overrides_built_in_tool: bool = False, skip_permission: bool = False, defer: Literal["auto", "never"] | None = None, + metadata: dict[str, Any] | None = None, ) -> Callable[[Callable[..., Any]], Tool]: pass @@ -91,6 +105,7 @@ def define_tool( overrides_built_in_tool: bool = False, skip_permission: bool = False, defer: Literal["auto", "never"] | None = None, + metadata: dict[str, Any] | None = None, ) -> Tool: pass @@ -105,6 +120,7 @@ def define_tool( overrides_built_in_tool: bool = False, skip_permission: bool = False, defer: Literal["auto", "never"] | None = None, + metadata: dict[str, Any] | None = None, ) -> Tool: pass @@ -118,6 +134,7 @@ def define_tool( overrides_built_in_tool: bool = False, skip_permission: bool = False, defer: Literal["auto", "never"] | None = None, + metadata: dict[str, Any] | None = None, ) -> Tool | Callable[[Callable[[Any, ToolInvocation], Any]], Tool]: """ Define a tool with automatic JSON schema generation from Pydantic models. @@ -166,6 +183,10 @@ def lookup_issue(params: LookupIssueParams) -> str: rather than always pre-loaded. When "auto", the tool can be deferred and surfaced through tool search. When "never", the tool is always pre-loaded. Optional; defaults to "auto". + metadata: Opaque, host-defined metadata associated with the tool definition. + Keys are namespaced and not part of the stable public API; values + are not interpreted and may be recognized to inform host-specific + behavior. Unknown keys are preserved. Returns: A Tool instance @@ -211,7 +232,21 @@ async def wrapped_handler(invocation: ToolInvocation) -> ToolResult: if takes_params: args = invocation.arguments or {} if ptype is not None and _is_pydantic_model(ptype): - call_args.append(ptype.model_validate(args)) + try: + call_args.append(ptype.model_validate(args)) + except ValidationError as exc: + # Highlight input validation problems to the LLM. + parts = [] + for err in exc.errors(): + loc = ".".join(map(str, err["loc"])) + msg = err["msg"] + parts.append(f"{loc}: {msg}" if loc else msg) + return ToolResult( + text_result_for_llm="Invalid tool arguments:\n" + "\n".join(parts), + result_type="failure", + error=str(exc), + tool_telemetry={}, + ) else: call_args.append(args) if takes_invocation: @@ -246,6 +281,7 @@ async def wrapped_handler(invocation: ToolInvocation) -> ToolResult: overrides_built_in_tool=overrides_built_in_tool, skip_permission=skip_permission, defer=defer, + metadata=metadata, ) # If handler is provided, call decorator immediately @@ -265,6 +301,7 @@ async def wrapped_handler(invocation: ToolInvocation) -> ToolResult: overrides_built_in_tool=overrides_built_in_tool, skip_permission=skip_permission, defer=defer, + metadata=metadata, ) # Otherwise return decorator for @define_tool(...) usage diff --git a/python/e2e/conftest.py b/python/e2e/conftest.py index 7409cf556b..f441097f32 100644 --- a/python/e2e/conftest.py +++ b/python/e2e/conftest.py @@ -5,7 +5,19 @@ import pytest import pytest_asyncio -from .testharness import E2ETestContext +from .testharness import E2ETestContext, is_inprocess_transport + +# Host-side auth resolution ranks HMAC above the GitHub token, so an ambient +# COPILOT_HMAC_KEY (CI sets one as a job-level credential) would be picked over +# the token the replay snapshots expect, yielding 401s. For the in-process +# transport the runtime is hosted in this test process and can capture the key as +# early as client construction, so neutralize it at module load — the analogue of +# .NET's InProcessEnvIsolation [ModuleInitializer] and Node's module-init guard. +# 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 is_inprocess_transport(): + os.environ.pop("COPILOT_HMAC_KEY", None) + os.environ.pop("CAPI_HMAC_KEY", None) @pytest.hookimpl(tryfirst=True, hookwrapper=True) diff --git a/python/e2e/test_copilot_request_session_id_e2e.py b/python/e2e/test_copilot_request_session_id_e2e.py index e40af13a1c..81624d73d0 100644 --- a/python/e2e/test_copilot_request_session_id_e2e.py +++ b/python/e2e/test_copilot_request_session_id_e2e.py @@ -36,6 +36,9 @@ class _InterceptedRequest: url: str session_id: str | None + agent_id: str | None + parent_agent_id: str | None + interaction_type: str | None class _SessionIdHandler(CopilotRequestHandler): @@ -46,7 +49,15 @@ async def send_request( self, request: httpx.Request, ctx: CopilotRequestContext ) -> httpx.Response: url = str(request.url) - self.records.append(_InterceptedRequest(url=url, session_id=ctx.session_id)) + self.records.append( + _InterceptedRequest( + url=url, + session_id=ctx.session_id, + agent_id=ctx.agent_id, + parent_agent_id=ctx.parent_agent_id, + interaction_type=ctx.interaction_type, + ) + ) if is_inference_url(url): return build_inference_response(request) # Force /responses transport so the inference URL is predictable. @@ -56,6 +67,11 @@ async def send_request( session_id_client = isolated_client_fixture(_SessionIdHandler) +def _assert_agent_metadata(record: _InterceptedRequest) -> None: + assert record.agent_id + assert record.interaction_type + + class TestCopilotRequestSessionId: capi_session_id: str | None = None @@ -78,6 +94,7 @@ async def test_threads_session_id_into_capi_session(self, session_id_client): assert r.session_id == session.session_id, ( "CAPI inference request must carry the runtime session id" ) + _assert_agent_metadata(r) # Validate the final assistant response arrived (guards against truncated captures) assert "OK from the synthetic" in text @@ -112,6 +129,7 @@ async def test_threads_session_id_into_byok_session(self, session_id_client): assert r.session_id == byok_session_id, ( "BYOK inference request must carry the runtime session id" ) + _assert_agent_metadata(r) # Session ids are per-session, so the two turns must differ. assert byok_session_id != TestCopilotRequestSessionId.capi_session_id diff --git a/python/e2e/test_inprocess_ffi_e2e.py b/python/e2e/test_inprocess_ffi_e2e.py new file mode 100644 index 0000000000..c119c4ea4e --- /dev/null +++ b/python/e2e/test_inprocess_ffi_e2e.py @@ -0,0 +1,40 @@ +"""E2E smoke test for the in-process (FFI) transport. + +Starts a client over the in-process FFI transport, performs a ``ping`` +round-trip through the native runtime library, and stops cleanly. Resolution of +the transport from ``COPILOT_SDK_DEFAULT_CONNECTION`` is exercised by the full +E2E suite running under the ``inprocess`` CI matrix cell, not here. + +Mirrors nodejs/test/e2e/inprocess_ffi.e2e.test.ts. +""" + +from __future__ import annotations + +import pytest + +from copilot import CopilotClient, RuntimeConnection + +from .testharness import E2ETestContext +from .testharness.context import get_cli_path_for_tests + +pytestmark = pytest.mark.asyncio(loop_scope="module") + + +class TestInProcessFfi: + async def test_should_start_and_connect_over_in_process_ffi( + self, ctx: E2ETestContext, monkeypatch: pytest.MonkeyPatch + ): + # In-process hosting loads the runtime cdylib next to the resolved CLI + # entrypoint and lets the native host spawn the worker. ``ping`` is a + # purely local RPC round-trip, so no auth or replay proxy is involved. + # If the native library is unavailable, start() raises and the test fails. + monkeypatch.setenv("COPILOT_CLI_PATH", get_cli_path_for_tests()) + client = CopilotClient(connection=RuntimeConnection.for_inprocess()) + await client.start() + + try: + pong = await client.ping("ffi message") + assert pong.message == "pong: ffi message" + assert pong.timestamp is not None + finally: + await client.stop() diff --git a/python/e2e/test_mode_handlers_e2e.py b/python/e2e/test_mode_handlers_e2e.py index 7ef9519f4a..f6173a4a5e 100644 --- a/python/e2e/test_mode_handlers_e2e.py +++ b/python/e2e/test_mode_handlers_e2e.py @@ -35,7 +35,7 @@ async def mode_ctx(ctx: E2ETestContext): """Configure per-token user responses for mode-handler tests.""" proxy_url = ctx.proxy_url - ctx.client._options.env["COPILOT_DEBUG_GITHUB_API_URL"] = proxy_url + ctx.add_runtime_env("COPILOT_DEBUG_GITHUB_API_URL", proxy_url) await ctx.set_copilot_user_by_token( MODE_HANDLER_TOKEN, @@ -119,7 +119,7 @@ async def on_exit_plan_mode_request(request, invocation): assert len(exit_plan_mode_requests) == 1 request = exit_plan_mode_requests[0] assert request["summary"] == PLAN_SUMMARY - assert request["actions"] == ["interactive", "autopilot", "exit_only"] + assert request["actions"] == ["autopilot", "interactive", "exit_only"] assert request["recommendedAction"] == "interactive" assert request.get("planContent") is not None diff --git a/python/e2e/test_per_session_auth_e2e.py b/python/e2e/test_per_session_auth_e2e.py index 776408a799..a8d13dc1de 100644 --- a/python/e2e/test_per_session_auth_e2e.py +++ b/python/e2e/test_per_session_auth_e2e.py @@ -18,7 +18,7 @@ async def auth_ctx(ctx: E2ETestContext): # Redirect GitHub API calls to the proxy so per-session auth token # resolution (fetchCopilotUser) is intercepted. Must be set before the # CLI subprocess is spawned (i.e., before the first create_session call). - ctx.client._options.env["COPILOT_DEBUG_GITHUB_API_URL"] = proxy_url + ctx.add_runtime_env("COPILOT_DEBUG_GITHUB_API_URL", proxy_url) await ctx.set_copilot_user_by_token( "token-alice", diff --git a/python/e2e/test_rpc_server_e2e.py b/python/e2e/test_rpc_server_e2e.py index fb422d5b31..bcef26754b 100644 --- a/python/e2e/test_rpc_server_e2e.py +++ b/python/e2e/test_rpc_server_e2e.py @@ -85,7 +85,7 @@ def _paths_equal(left: str, right: str | None) -> bool: @pytest.fixture(scope="module") async def authed_ctx(ctx: E2ETestContext): """Configure proxy to redirect GitHub user lookups so per-token auth works.""" - ctx.client._options.env["COPILOT_DEBUG_GITHUB_API_URL"] = ctx.proxy_url + ctx.add_runtime_env("COPILOT_DEBUG_GITHUB_API_URL", ctx.proxy_url) return ctx diff --git a/python/e2e/test_subagent_hooks_e2e.py b/python/e2e/test_subagent_hooks_e2e.py index 1ca2a54c12..da70265a04 100644 --- a/python/e2e/test_subagent_hooks_e2e.py +++ b/python/e2e/test_subagent_hooks_e2e.py @@ -3,10 +3,14 @@ fire for tool calls made by sub-agents spawned via the task tool. """ +from __future__ import annotations + import os +import httpx import pytest +from copilot import CopilotRequestContext, CopilotRequestHandler from copilot.client import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler @@ -16,12 +20,56 @@ pytestmark = pytest.mark.asyncio(loop_scope="module") +class _RecordingRequestHandler(CopilotRequestHandler): + def __init__(self) -> None: + self.records: list[dict[str, str | None]] = [] + + async def send_request( + self, request: httpx.Request, ctx: CopilotRequestContext + ) -> httpx.Response: + self.records.append( + { + "url": str(request.url), + "agent_id": ctx.agent_id, + "parent_agent_id": ctx.parent_agent_id, + "interaction_type": ctx.interaction_type, + } + ) + return await super().send_request(request, ctx) + + +def _is_inference_url(url: str) -> bool: + u = url.lower() + return ( + u.endswith("/chat/completions") + or u.endswith("/responses") + or u.endswith("/v1/messages") + or u.endswith("/messages") + ) + + +def _assert_subagent_request_metadata(records: list[dict[str, str | None]]) -> None: + inference = [r for r in records if _is_inference_url(r["url"] or "")] + assert len(inference) > 0, "request handler should observe inference requests" + + subagent_request = next((r for r in inference if r["parent_agent_id"]), None) + assert subagent_request is not None, ( + "sub-agent inference request should carry a parent_agent_id" + ) + assert subagent_request["agent_id"], "sub-agent inference request should carry an agent_id" + assert subagent_request["interaction_type"], ( + "sub-agent inference request should carry an interaction_type" + ) + assert subagent_request["parent_agent_id"] != subagent_request["agent_id"] + + class TestSubagentHooks: async def test_should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls( self, ctx: E2ETestContext ): """Test that preToolUse/postToolUse hooks fire for sub-agent tool calls""" hook_log = [] + request_handler = _RecordingRequestHandler() async def on_pre_tool_use(input_data, invocation): hook_log.append( @@ -54,6 +102,7 @@ async def on_post_tool_use(input_data, invocation): working_directory=ctx.work_dir, env=env, github_token=github_token, + request_handler=request_handler, ) session = await client.create_session( @@ -87,6 +136,7 @@ async def on_post_tool_use(input_data, invocation): assert view_pre[0]["sessionId"] != task_pre[0]["sessionId"], ( "Sub-agent tool hooks should have a different sessionId than parent tool hooks" ) + _assert_subagent_request_metadata(request_handler.records) await session.disconnect() await client.stop() diff --git a/python/e2e/testharness/__init__.py b/python/e2e/testharness/__init__.py index 83f548eaa0..75ce76d9c5 100644 --- a/python/e2e/testharness/__init__.py +++ b/python/e2e/testharness/__init__.py @@ -1,6 +1,6 @@ """Test harness for E2E tests.""" -from .context import CLI_PATH, DEFAULT_GITHUB_TOKEN, E2ETestContext +from .context import CLI_PATH, DEFAULT_GITHUB_TOKEN, E2ETestContext, is_inprocess_transport from .helper import get_final_assistant_message, get_next_event_of_type, wait_for_condition from .proxy import CapiProxy @@ -12,4 +12,5 @@ "get_final_assistant_message", "get_next_event_of_type", "wait_for_condition", + "is_inprocess_transport", ] diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py index de1bf03292..679a36e28c 100644 --- a/python/e2e/testharness/context.py +++ b/python/e2e/testharness/context.py @@ -46,6 +46,15 @@ def get_cli_path_for_tests() -> str: DEFAULT_GITHUB_TOKEN = "fake-token-for-e2e-tests" +def is_inprocess_transport() -> bool: + """Return True when the E2E suite should run over the in-process (FFI) transport. + + Selected by the ``inprocess`` CI matrix cell via + ``COPILOT_SDK_DEFAULT_CONNECTION=inprocess``. Mirrors the Node/.NET harnesses. + """ + return (os.environ.get("COPILOT_SDK_DEFAULT_CONNECTION") or "").lower() == "inprocess" + + class E2ETestContext: """Holds shared resources for E2E tests.""" @@ -56,6 +65,10 @@ def __init__(self): self.proxy_url: str = "" self._proxy: CapiProxy | None = None self._client: CopilotClient | None = None + self._inprocess: bool = is_inprocess_transport() + self._client_inprocess: bool = False + self._restore_env: list[tuple[str, str | None]] = [] + self._restore_cwd: str | None = None async def setup(self, cli_args: list[str] | None = None): """Set up the test context with a shared client. @@ -83,16 +96,87 @@ async def setup(self, cli_args: list[str] | None = None): }, ) - # Create the shared client (like Node.js/Go do) - self._client = CopilotClient( - connection=RuntimeConnection.for_stdio( - path=self.cli_path, - args=tuple(cli_args or []), - ), - working_directory=self.work_dir, - env=self.get_env(), - github_token=DEFAULT_GITHUB_TOKEN, + # Create the shared client (like Node.js/Go do). The in-process (FFI) + # transport loads the runtime into this test host process, so it cannot + # honor a per-client working_directory or env block: the worker inherits + # this process's ambient cwd and environment. We therefore mirror the + # per-test redirects, isolated home, and credentials onto the real process + # (os.environ writes reach native getenv on CPython) and chdir into the + # work dir, then create the client without working_directory/env. This + # matches the Node/.NET in-process harnesses. + self._client_inprocess = self._inprocess and not cli_args + if self._client_inprocess: + self._apply_inprocess_environment() + self._client = CopilotClient( + connection=RuntimeConnection.for_inprocess(), + github_token=DEFAULT_GITHUB_TOKEN, + ) + else: + self._client = CopilotClient( + connection=RuntimeConnection.for_stdio( + path=self.cli_path, + args=tuple(cli_args or []), + ), + working_directory=self.work_dir, + env=self.get_env(), + github_token=DEFAULT_GITHUB_TOKEN, + ) + + def _apply_inprocess_environment(self) -> None: + """Mirror the isolated test environment onto the real process for in-process hosting. + + The in-process worker inherits this process's environment and cwd at + spawn, so the per-test redirects must live on ``os.environ`` and the + process cwd. Auth flows via GH_TOKEN/GITHUB_TOKEN (the FFI argv omits the + stdio ``--auth-token-env`` wiring) and HMAC is disabled so host-side auth + resolution matches the replay snapshots. Restored in ``teardown``. + """ + inprocess_env = dict(self.get_env()) + inprocess_env.update( + { + "GH_TOKEN": DEFAULT_GITHUB_TOKEN, + "GITHUB_TOKEN": DEFAULT_GITHUB_TOKEN, + "COPILOT_CLI_PATH": self.cli_path, + "COPILOT_HMAC_KEY": "", + "CAPI_HMAC_KEY": "", + } ) + for key, value in inprocess_env.items(): + self._restore_env.append((key, os.environ.get(key))) + os.environ[key] = value + + self._restore_cwd = os.getcwd() + os.chdir(self.work_dir) + + def add_runtime_env(self, key: str, value: str) -> None: + """Set an env var seen by the runtime, honoring the active transport. + + Child-process transports read env from the client's env block, but the + in-process worker inherits *this* process's environment, so the var must + live on ``os.environ`` (and be restored in teardown). Must be called + before the runtime starts (i.e., before the first ``create_session``). + """ + if self._client_inprocess: + self._restore_env.append((key, os.environ.get(key))) + os.environ[key] = value + else: + options = self.client._options + if options.env is None: + options.env = {} + options.env[key] = value + + def _restore_inprocess_environment(self) -> None: + """Undo the in-process environment mirror and cwd change from setup.""" + for key, previous in reversed(self._restore_env): + if previous is None: + os.environ.pop(key, None) + else: + os.environ[key] = previous + self._restore_env = [] + if self._restore_cwd is not None: + with contextlib.suppress(OSError): + os.chdir(self._restore_cwd) + self._restore_cwd = None async def teardown(self, test_failed: bool = False): """Clean up the test context. @@ -107,6 +191,9 @@ async def teardown(self, test_failed: bool = False): pass # stop() completes all cleanup before raising; safe to ignore in teardown self._client = None + if self._client_inprocess: + self._restore_inprocess_environment() + if self._proxy: await self._proxy.stop(skip_writing_cache=test_failed) self._proxy = None diff --git a/python/test_canvas.py b/python/test_canvas.py index 8bcb79a230..684cef6b79 100644 --- a/python/test_canvas.py +++ b/python/test_canvas.py @@ -14,6 +14,7 @@ CanvasDeclaration, CanvasError, CanvasHandler, + CanvasProviderIdentity, ExtensionInfo, OpenCanvasInstance, ) @@ -67,6 +68,16 @@ def test_extension_info_serializes(): assert info.to_dict() == {"source": "github-app", "name": "my-ext"} +def test_canvas_provider_identity_serializes(): + provider = CanvasProviderIdentity(id="app:builtin:window-1", name="Built-in") + assert provider.to_dict() == {"id": "app:builtin:window-1", "name": "Built-in"} + + +def test_canvas_provider_identity_drops_optional_name(): + provider = CanvasProviderIdentity(id="app:builtin:window-1") + assert provider.to_dict() == {"id": "app:builtin:window-1"} + + def test_canvas_open_response_drops_none_fields(): assert CanvasProviderOpenResult().to_dict() == {} assert CanvasProviderOpenResult(url="https://x", status="ok").to_dict() == { diff --git a/python/test_cli_download.py b/python/test_cli_download.py new file mode 100644 index 0000000000..36952919df --- /dev/null +++ b/python/test_cli_download.py @@ -0,0 +1,53 @@ +"""Tests for the in-process runtime library download integrity checks.""" + +from __future__ import annotations + +import base64 +import hashlib +from unittest.mock import patch + +import pytest + +from copilot import _cli_download + + +def _integrity(data: bytes, algo: str = "sha512") -> str: + digest = hashlib.new(algo, data).digest() + return f"{algo}-{base64.b64encode(digest).decode('ascii')}" + + +class TestVerifyIntegrity: + def test_accepts_matching_checksum(self): + data = b"native-library-bytes" + _cli_download._verify_integrity(data, _integrity(data)) + + def test_rejects_mismatched_checksum(self): + with pytest.raises(RuntimeError, match="Integrity mismatch"): + _cli_download._verify_integrity(b"tampered", _integrity(b"original")) + + 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") + + +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") + + 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") + + # The library bytes must never be extracted/written when verification is impossible. + extract.assert_not_called() diff --git a/python/test_client.py b/python/test_client.py index 5e1b8be634..f2fb059ab2 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -5,14 +5,17 @@ """ import asyncio +import inspect from datetime import UTC, datetime from unittest.mock import AsyncMock, Mock, patch import pytest from copilot import ( + CanvasProviderIdentity, CapiSessionOptions, CopilotClient, + ExtensionInfo, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext, RuntimeConnection, @@ -37,9 +40,18 @@ SessionEvent, SessionEventType, ) +from copilot.tools import Tool from e2e.testharness import CLI_PATH +def test_inprocess_connection_has_no_child_process_options(): + connection = RuntimeConnection.for_inprocess() + + assert list(inspect.signature(RuntimeConnection.for_inprocess).parameters) == [] + assert not hasattr(connection, "path") + assert not hasattr(connection, "args") + + class TestClientShutdown: @pytest.mark.asyncio async def test_stop_requests_runtime_shutdown_for_owned_process(self): @@ -556,6 +568,89 @@ async def mock_request(method, params, **kwargs): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_create_and_resume_session_forward_tool_metadata(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured = {} + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method in ("session.create", "session.resume"): + result = {"sessionId": params.get("sessionId") or "session-1"} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + return {} + + client._client.request = mock_request + metadata = {"github.com/copilot:safeForTelemetry": {"name": True, "inputsNames": False}} + tool = Tool(name="my_tool", description="a tool", metadata=metadata) + plain_tool = Tool(name="plain_tool", description="a tool") + + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + tools=[tool, plain_tool], + ) + await client.resume_session( + session.session_id, + on_permission_request=PermissionHandler.approve_all, + tools=[tool], + ) + + create_tools = captured["session.create"]["tools"] + assert create_tools[0]["metadata"] == metadata + # Omitted when unset. + assert "metadata" not in create_tools[1] + assert captured["session.resume"]["tools"][0]["metadata"] == metadata + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_create_and_resume_session_forward_canvas_provider(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured = {} + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method in ("session.create", "session.resume"): + result = {"sessionId": params.get("sessionId") or "session-1"} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + return {} + + client._client.request = mock_request + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + extension_info=ExtensionInfo(source="github-app", name="counter"), + canvas_provider=CanvasProviderIdentity(id="app:builtin:window-1", name="Built-in"), + ) + await client.resume_session( + session.session_id, + on_permission_request=PermissionHandler.approve_all, + canvas_provider=CanvasProviderIdentity(id="app:builtin:window-1"), + ) + + assert captured["session.create"]["canvasProvider"] == { + "id": "app:builtin:window-1", + "name": "Built-in", + } + assert captured["session.create"]["extensionInfo"] == { + "source": "github-app", + "name": "counter", + } + assert captured["session.resume"]["canvasProvider"] == { + "id": "app:builtin:window-1", + } + finally: + await client.force_stop() + @pytest.mark.asyncio async def test_create_and_resume_session_forward_new_session_options(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) @@ -2191,6 +2286,32 @@ def test_model_field_is_omitted_when_absent(self): wire = client._convert_custom_agent_to_wire_format(agent) assert "model" not in wire + def test_reasoning_effort_is_forwarded_in_camel_case(self): + from copilot.client import CopilotClient + from copilot.session import CustomAgentConfig + + client = CopilotClient.__new__(CopilotClient) + agent: CustomAgentConfig = { + "name": "reasoning-agent", + "prompt": "Think carefully.", + "reasoning_effort": "high", + } + wire = client._convert_custom_agent_to_wire_format(agent) + assert wire["reasoningEffort"] == "high" + assert "reasoning_effort" not in wire + + def test_reasoning_effort_is_omitted_when_absent(self): + from copilot.client import CopilotClient + from copilot.session import CustomAgentConfig + + client = CopilotClient.__new__(CopilotClient) + agent: CustomAgentConfig = { + "name": "default-agent", + "prompt": "Use runtime defaults.", + } + wire = client._convert_custom_agent_to_wire_format(agent) + assert "reasoningEffort" not in wire + class TestPostToolUseFailureHookDispatch: """Unit tests for the postToolUseFailure handler dispatch.""" @@ -2521,12 +2642,33 @@ async def on_telemetry(notification): await client.force_stop() @pytest.mark.asyncio - async def test_event_handler_not_registered_without_option(self): + async def test_event_not_forwarded_without_option(self): + # Client-global handlers are always registered (so that hooks.invoke works), + # but without the on_github_telemetry option the telemetry adapter is inert: + # incoming events must not be forwarded to any callback. client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: - assert "gitHubTelemetry.event" not in client._client.notification_method_handlers - assert "gitHubTelemetry.event" not in client._client.request_handlers + assert client._on_github_telemetry is None + + # Dispatching a telemetry event is a harmless no-op when not opted in. + client._client._handle_message( + { + "jsonrpc": "2.0", + "method": "gitHubTelemetry.event", + "params": { + "sessionId": "sess-no-telemetry", + "restricted": False, + "event": { + "kind": "tool_call_executed", + "metrics": {"duration_ms": 1.0}, + "properties": {"tool": "shell"}, + "session_id": "sess-no-telemetry", + }, + }, + } + ) + await asyncio.sleep(0) finally: await client.force_stop() diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index 7f8c29b6e8..1ffbd59c54 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -49,6 +49,20 @@ def test_unknown_event_type_maps_to_unknown(self): event = session_event_from_dict(unknown_event) assert event.type == SessionEventType.UNKNOWN, f"Expected UNKNOWN, got {event.type}" + def test_internal_event_type_maps_to_unknown(self): + """Internal events should use the forward-compatible raw event path.""" + internal_event = { + "id": str(uuid4()), + "timestamp": datetime.now().isoformat(), + "parentId": None, + "type": "session.memory_changed", + "data": {}, + } + + event = session_event_from_dict(internal_event) + assert event.type == SessionEventType.UNKNOWN + assert session_event_to_dict(event)["type"] == "session.memory_changed" + def test_known_event_preserves_top_level_agent_id(self): """Known events should preserve the top-level sub-agent envelope ID.""" known_event = { diff --git a/python/test_tools.py b/python/test_tools.py index d583b59c01..90498c2b8e 100644 --- a/python/test_tools.py +++ b/python/test_tools.py @@ -3,9 +3,10 @@ import json import pytest -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field, field_validator from copilot import define_tool +from copilot.generated.rpc import ExternalToolTextResultForLlm from copilot.tools import ( ToolInvocation, ToolResult, @@ -197,6 +198,91 @@ def failing_tool(params: Params, invocation: ToolInvocation) -> str: # But the actual error is stored internally assert result.error == "secret error message" + async def test_validation_error_is_surfaced_to_llm(self): + class Params(BaseModel): + username: str + + @field_validator("username") + @classmethod + def check_username(cls, v: str) -> str: + if v == "admin": + raise ValueError("username 'admin' is reserved") + return v + + @define_tool("validate", description="A validating tool") + def validating_tool(params: Params) -> str: + return "ok" + + invocation = ToolInvocation( + session_id="s1", + tool_call_id="c1", + tool_name="validate", + arguments={"username": "admin"}, + ) + + result = await validating_tool.handler(invocation) + + assert result.result_type == "failure" + assert result.text_result_for_llm.startswith("Invalid tool arguments:") + assert "username 'admin' is reserved" in result.text_result_for_llm + # Full detail is retained in the debug field. + assert result.error is not None + + async def test_validation_error_extra_forbid_includes_field_name(self): + class Params(BaseModel): + model_config = ConfigDict(extra="forbid") + + request: str + + @define_tool("strict", description="A strict tool") + def strict_tool(params: Params) -> str: + return "ok" + + invocation = ToolInvocation( + session_id="s1", + tool_call_id="c1", + tool_name="strict", + arguments={"request": "ok", "extra_field": "unexpected"}, + ) + + result = await strict_tool.handler(invocation) + + assert result.result_type == "failure" + assert result.text_result_for_llm.startswith("Invalid tool arguments:") + # The offending key name is carried in `loc` even though the generic + # message is "Extra inputs are not permitted". + assert "extra_field" in result.text_result_for_llm + assert result.error is not None + + async def test_validation_error_from_handler_body_is_redacted(self): + class Params(BaseModel): + pass + + class Internal(BaseModel): + count: int + + @define_tool("body", description="A tool that validates internally") + def body_tool(params: Params) -> str: + Internal.model_validate({"count": "secret-not-an-int"}) + return "ok" + + invocation = ToolInvocation( + session_id="s1", + tool_call_id="c1", + tool_name="body", + arguments={}, + ) + + result = await body_tool.handler(invocation) + + assert result.result_type == "failure" + # A ValidationError from the handler body must not be surfaced as an + # argument-validation error; it stays redacted like any other exception. + assert not result.text_result_for_llm.startswith("Invalid tool arguments:") + assert "secret-not-an-int" not in result.text_result_for_llm + assert "error" in result.text_result_for_llm.lower() + assert result.error is not None + async def test_function_style_api(self): class Params(BaseModel): value: str @@ -427,3 +513,40 @@ def test_call_tool_result_dict_is_json_serialized_by_normalize(self): result = _normalize_result({"content": [{"type": "text", "text": "hello"}]}) parsed = json.loads(result.text_result_for_llm) assert parsed == {"content": [{"type": "text", "text": "hello"}]} + + +class TestToolReferences: + def test_tool_references_pass_through_normalize(self): + input_result = ToolResult( + text_result_for_llm="found 2 tools", + result_type="success", + tool_references=["get_weather", "check_status"], + ) + result = _normalize_result(input_result) + assert result.tool_references == ["get_weather", "check_status"] + + def test_tool_references_serialized_to_wire(self): + wire = ExternalToolTextResultForLlm( + text_result_for_llm="found 2 tools", + result_type="success", + tool_references=["get_weather", "check_status"], + ) + data = wire.to_dict() + assert data["toolReferences"] == ["get_weather", "check_status"] + + def test_tool_references_omitted_when_none(self): + wire = ExternalToolTextResultForLlm( + text_result_for_llm="ok", + result_type="success", + ) + assert "toolReferences" not in wire.to_dict() + + def test_tool_references_round_trip_from_wire(self): + wire = ExternalToolTextResultForLlm.from_dict( + { + "textResultForLlm": "found tools", + "resultType": "success", + "toolReferences": ["alpha", "beta"], + } + ) + assert wire.tool_references == ["alpha", "beta"] diff --git a/rust/.gitignore b/rust/.gitignore index c4095ffc0f..c149fa3946 100644 --- a/rust/.gitignore +++ b/rust/.gitignore @@ -1,3 +1,4 @@ /target Cargo.lock.bak cli-version.txt +cli-version-in-process.txt diff --git a/rust/Cargo.lock b/rust/Cargo.lock index fb7b66e198..8de6797989 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -433,6 +433,9 @@ dependencies = [ "futures-util", "getrandom 0.2.17", "http", + "indexmap", + "libloading", + "native-tls", "parking_lot", "regex", "reqwest", @@ -773,6 +776,16 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libredox" version = "0.1.16" @@ -1217,9 +1230,7 @@ version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ - "log", "once_cell", - "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -1836,11 +1847,9 @@ checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" dependencies = [ "base64", "log", + "native-tls", "once_cell", - "rustls", - "rustls-pki-types", "url", - "webpki-roots 0.26.11", ] [[package]] @@ -2033,24 +2042,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.7", -] - -[[package]] -name = "webpki-roots" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "windows-link" version = "0.2.1" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 66ef69ad21..0f18a9b159 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -13,6 +13,7 @@ readme = "README.md" license = "MIT" include = [ "src/**/*", + "build/**/*", "examples/**/*", "tests/**/*", "build.rs", @@ -20,6 +21,7 @@ include = [ "README.md", "LICENSE", "cli-version.txt", + "cli-version-in-process.txt", ] [lib] @@ -28,6 +30,7 @@ name = "github_copilot_sdk" [features] default = ["bundled-cli"] bundled-cli = ["dep:tar", "dep:flate2", "dep:zip"] +bundled-in-process = ["bundled-cli", "dep:libloading"] derive = ["dep:schemars"] test-support = [] @@ -40,6 +43,7 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] async-trait = "0.1" +indexmap = { version = "2", features = ["serde"] } schemars = { version = "1", optional = true } serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -48,10 +52,13 @@ tokio-stream = { version = "0.1", features = ["sync"] } tokio-util = { version = "0.7", default-features = false } tracing = "0.1" dirs = "5" +libloading = { version = "0.8", optional = true } parking_lot = "0.12" regex = "1" getrandom = "0.2" uuid = { version = "1", default-features = false, features = ["v4"] } +flate2 = { version = "1", optional = true } +tar = { version = "0.4", optional = true } # LLM inference callback transport: idiomatic HTTP/WebSocket forwarding for the # `CopilotRequestHandler`, plus base64/byte/stream plumbing for the chunk protocol. base64 = "0.22" @@ -64,10 +71,6 @@ tokio-tungstenite = { version = "0.24", default-features = false, features = ["c [target.'cfg(windows)'.dependencies] zip = { version = "2", default-features = false, features = ["deflate"], optional = true } -[target.'cfg(not(windows))'.dependencies] -flate2 = { version = "1", optional = true } -tar = { version = "0.4", optional = true } - [dev-dependencies] rusqlite = { version = "0.35", features = ["bundled"] } schemars = "1" @@ -88,9 +91,12 @@ name = "protocol_version_test" required-features = ["test-support"] [build-dependencies] +base64 = "0.22" dirs = "5" flate2 = "1" +serde_json = "1" sha2 = "0.10" tar = "0.4" -ureq = { version = "2", default-features = false, features = ["tls"] } +ureq = { version = "2", default-features = false, features = ["native-tls"] } +native-tls = "0.2" zip = { version = "2", default-features = false, features = ["deflate"] } diff --git a/rust/README.md b/rust/README.md index 6d92224088..254a2b2167 100644 --- a/rust/README.md +++ b/rust/README.md @@ -72,15 +72,15 @@ client.stop().await?; **`ClientOptions`:** -| Field | Type | Description | -| ------------- | --------------------------- | --------------------------------------------------------------- | -| `program` | `CliProgram` | `Resolve` (default: auto-detect) or `Path(PathBuf)` (explicit) | -| `prefix_args` | `Vec` | Args before `--server` (e.g. script path for node) | -| `cwd` | `PathBuf` | Working directory for CLI process | -| `env` | `Vec<(OsString, OsString)>` | Environment variables for CLI process | -| `env_remove` | `Vec` | Environment variables to remove | -| `extra_args` | `Vec` | Extra CLI flags | -| `transport` | `Transport` | `Stdio` (default), `Tcp { port }`, or `External { host, port }` | +| Field | Type | Description | +| ------------------- | --------------------------- | ----------------------------------------------------------------- | +| `program` | `CliProgram` | `Resolve` (default: auto-detect) or `Path(PathBuf)` (explicit) | +| `prefix_args` | `Vec` | Args before `--server` (e.g. script path for node) | +| `working_directory` | `PathBuf` | Working directory for CLI process (empty = host process's cwd) | +| `env` | `Vec<(OsString, OsString)>` | Environment variables for CLI process | +| `env_remove` | `Vec` | Environment variables to remove | +| `extra_args` | `Vec` | Extra CLI flags | +| `transport` | `Transport` | `Default`, `Stdio`, `InProcess`, `Tcp`, or `External` | With the default `CliProgram::Resolve`, `Client::start()` resolves the CLI in this order: an explicit `CliProgram::Path(path)`, the `COPILOT_CLI_PATH` env var, then the bundled CLI that was embedded at build time. There is no PATH scanning — if you've opted out of bundling (`default-features = false`) you must supply either `CliProgram::Path` or `COPILOT_CLI_PATH`. @@ -749,7 +749,7 @@ none of them are scheduled for removal. caller-supplied `AsyncRead` / `AsyncWrite`. Useful for testing, in-process embedding, or custom transports. Other SDKs are spawn-only or fixed-stdio. -- **`enum Transport { Stdio, Tcp, External }`** — explicit, exhaustive +- **`enum Transport { Default, Stdio, InProcess, Tcp, External }`** — explicit transport selector on `ClientOptions::transport`. Node/Python/Go rely on conditional config field combinations instead. - **Split `prefix_args` / `extra_args`** on `ClientOptions` — separate @@ -776,7 +776,18 @@ none of them are scheduled for removal. ## Embedded CLI -The SDK provisions the Copilot CLI binary at build time. By default the `bundled-cli` feature embeds the verified binary directly in your compiled crate, so end-user binaries are self-contained — no env var setup, no separate install, just `cargo build`. +The SDK provisions its runtime at build time. By default the `bundled-cli` +feature embeds the verified child-process runtime in your compiled crate. +Enable `bundled-in-process` to additionally embed the native runtime library +and use `Transport::InProcess`: + +```toml +github-copilot-sdk = { version = "0.1", features = ["bundled-in-process"] } +``` + +`CliProgram::Path` and raw `ClientOptions::extra_args` apply only to +child-process transports. Set `COPILOT_CLI_PATH` only when using an externally +provisioned compatible runtime package with in-process transport. For builds that prefer a smaller artifact, disable the `bundled-cli` feature: @@ -795,7 +806,7 @@ github-copilot-sdk = { version = "0.1", default-features = false } > together. > > **Convenience on the build machine only.** As a special case, -> `build.rs` downloads and SHA-verifies the compatible CLI version and +> `build.rs` downloads and integrity-verifies the compatible CLI version and > drops it into the build machine's per-user cache; the runtime > resolver on that same machine will pick it up automatically. This > makes local development and CI ergonomic, but it does **not** carry @@ -812,8 +823,11 @@ github-copilot-sdk = { version = "0.1", default-features = false } 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-appropriate archive from the [`github/copilot-cli` GitHub Releases](https://github.com/github/copilot-cli/releases) (`copilot-{platform}.tar.gz` on macOS/Linux, `.zip` on Windows), live-fetches the matching `SHA256SUMS.txt`, and verifies the archive hash. Then: - - **`bundled-cli` on (default, release):** embeds the raw archive bytes via `include_bytes!()`. Runtime extracts on first `Client::start()`. +2. **Build time:** `build.rs` downloads the platform-specific npm package and + verifies its `sha512` integrity against the lockfile or publish snapshot. + Then: + - **`bundled-cli` on (default):** creates and embeds a minimal archive containing only the CLI executable. + - **`bundled-in-process` on:** the minimal archive additionally contains the platform-native runtime library (`.dll`, `.so`, or `.dylib`); no other npm package files are embedded. - **`bundled-cli` off:** extracts the binary directly into the platform cache (staging file + atomic rename), idempotent across rebuilds. If the extracted binary is already present at the expected path, the download is skipped entirely — the extracted binary *is* the cache. 3. **Runtime:** in both modes the binary lives at: @@ -899,10 +913,11 @@ Supported: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win32-x64` ## Features -| Feature | Default | Description | -| -------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `bundled-cli` | ✓ | Build-time CLI embedding. Pulls in `tar`+`flate2` (Linux/macOS) or `zip` (Windows). Disable via `default-features = false` to opt out (e.g. when shipping a smaller binary or when always supplying the CLI via `CliProgram::Path` / `COPILOT_CLI_PATH`). | -| `derive` | — | `schema_for::()` for generating JSON Schema from Rust types (adds `schemars`). Enable when defining [tool parameters](#tool-registration). | +| Feature | Default | Description | +| ------- | ------- | ----------- | +| `bundled-cli` | ✓ | Embeds only the CLI executable. Disable via `default-features = false` when supplying the CLI via `CliProgram::Path` or `COPILOT_CLI_PATH`. | +| `bundled-in-process` | — | Enables `Transport::InProcess`, implies `bundled-cli`, and additionally embeds only the platform-native runtime library. | +| `derive` | — | `schema_for::()` for generating JSON Schema from Rust types (adds `schemars`). | ```toml # These examples use registry syntax for illustration; until the crate is @@ -911,7 +926,10 @@ Supported: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win32-x64` # Default — bundles the Copilot CLI in your binary. github-copilot-sdk = "0.1" -# Opt out of bundling — resolve CLI from COPILOT_CLI_PATH or system PATH instead. +# Enable the in-process transport and bundle its native runtime library. +github-copilot-sdk = { version = "0.1", features = ["bundled-in-process"] } + +# Opt out of bundling — supply the CLI explicitly at runtime. github-copilot-sdk = { version = "0.1", default-features = false } # Derive JSON Schema for tool parameters (adds to default bundled-cli). diff --git a/rust/build.rs b/rust/build.rs index 66d1de7bc8..d04cf2870b 100644 --- a/rust/build.rs +++ b/rust/build.rs @@ -1,709 +1,11 @@ -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; -use std::time::Duration; +#[cfg(feature = "bundled-in-process")] +#[path = "build/in_process.rs"] +mod implementation; -use sha2::Digest; +#[cfg(not(feature = "bundled-in-process"))] +#[path = "build/out_of_process.rs"] +mod implementation; fn main() { - println!("cargo:rerun-if-env-changed=DOCS_RS"); - println!("cargo:rerun-if-env-changed=COPILOT_SKIP_CLI_DOWNLOAD"); - println!("cargo:rerun-if-env-changed=COPILOT_CLI_EXTRACT_DIR"); - 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"); - - // Only declare the lockfile rerun when the lockfile 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.txt` is canonical. - let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"); - let lockfile = Path::new(&manifest_dir) - .join("..") - .join("nodejs") - .join("package-lock.json"); - if lockfile.is_file() { - println!("cargo:rerun-if-changed={}", lockfile.display()); - } - - // Hard opt-out: disable the entire download / bundle / cache mechanism - // in one step. For consumers who always supply the CLI via - // `CliProgram::Path` or `COPILOT_CLI_PATH` and don't want build.rs to - // touch the network (offline builds, locked-down CI, etc.). Works - // regardless of the `bundled-cli` cargo feature state — with neither - // `has_bundled_cli` nor `has_extracted_cli` emitted, runtime resolution - // falls straight through to `Error::BinaryNotFound` unless an explicit - // path source resolves first. - if std::env::var_os("COPILOT_SKIP_CLI_DOWNLOAD").is_some() { - println!( - "cargo:warning=COPILOT_SKIP_CLI_DOWNLOAD is set — skipping CLI download/bundle/cache" - ); - return; - } - - // docs.rs builds in a sandboxed environment without network access. - // Skip the CLI download so documentation can be generated successfully. - if std::env::var_os("DOCS_RS").is_some() { - println!("cargo:warning=DOCS_RS is set — skipping CLI download/bundle/cache"); - return; - } - - let Some(platform) = target_platform() else { - println!("cargo:warning=Unsupported target platform for Copilot CLI bundling — skipping"); - return; - }; - - let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR is always set by cargo"); - let out = Path::new(&out_dir); - - // 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. - let (version, expected_hash) = resolve_version_and_hash(platform.asset_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", - // consumed by both the embed-mode path computation in embeddedcli.rs - // and the runtime path computation in resolve.rs (when `bundled-cli` - // is off). It's a small, machine-independent datum: no absolute - // paths, no username/home leakage, so sccache / cross-machine - // `target/` reuse stays cache-coherent. - println!("cargo:rustc-env=COPILOT_SDK_CLI_VERSION={version}"); - - let base_url = format!("https://github.com/github/copilot-cli/releases/download/v{version}"); - let cache_dir = std::env::var("BUNDLED_CLI_CACHE_DIR") - .ok() - .map(std::path::PathBuf::from); - - // Versioned cache key since copilot asset names don't include the version. - let cache_key = format!("v{version}-{}", platform.asset_name); - - if std::env::var_os("CARGO_FEATURE_BUNDLED_CLI").is_some() { - // Embed mode: we need the archive bytes to bake into the rlib, so - // always run the download (cache hit short-circuits inside - // `cached_download`). - let archive = cached_download( - &format!("{base_url}/{}", platform.asset_name), - &cache_key, - &expected_hash, - &cache_dir, - ); - verify_binary_present_in_archive(&archive, platform.binary_name, platform.asset_name); - emit_embedded(out, &archive); - println!("cargo:rustc-cfg=has_bundled_cli"); - } else { - // With `bundled-cli` off the extracted binary *is* the cache. - // Skip the upstream download entirely when it already exists at - // the expected path. No two separate caches. - // - // Runtime resolution (see `src/resolve.rs::extracted_cli_path`) - // recomputes this same path from `COPILOT_SDK_CLI_VERSION` + the - // OS-derived binary name + optional `COPILOT_CLI_EXTRACT_DIR`, - // so we don't bake an absolute path into the crate. - let install_dir = extracted_install_dir(&version); - let final_path = install_dir.join(platform.binary_name); - - // Invalidate build.rs whenever the cached binary disappears (cache GC, - // manual rm, OS reset, switching extract dir). Without this, cargo - // replays the saved `has_extracted_cli` cfg from its build-script - // output cache even when the file is gone, and runtime resolution - // fails with BinaryNotFound. - println!("cargo:rerun-if-changed={}", final_path.display()); - - if !final_path.is_file() { - let archive = cached_download( - &format!("{base_url}/{}", platform.asset_name), - &cache_key, - &expected_hash, - &cache_dir, - ); - verify_binary_present_in_archive(&archive, platform.binary_name, platform.asset_name); - extract_to_cache(&archive, &install_dir, platform); - } - - // Re-check after potential download+extract above; not an `else` - // because we need to verify the extraction actually produced the file. - if final_path.is_file() { - println!("cargo:rustc-cfg=has_extracted_cli"); - } - } -} - -/// Install directory used when `bundled-cli` is off. Mirrors the runtime -/// convention in `src/resolve.rs::extracted_cli_path`: both sides MUST -/// compute the same path from the same inputs, otherwise the runtime -/// resolver won't find what build.rs extracted. -/// -/// If `COPILOT_CLI_EXTRACT_DIR` is set the binary lives directly under -/// that directory (no per-version subdir) — useful for vendored slots and -/// for `.cargo/config.toml [env]`-style pinning that's symmetric between -/// build-time write and runtime read. Otherwise the binary lives under -/// `/github-copilot-sdk/cli//`. -fn extracted_install_dir(version: &str) -> PathBuf { - if let Some(custom) = std::env::var_os("COPILOT_CLI_EXTRACT_DIR") { - PathBuf::from(custom) - } else { - let cache = dirs::cache_dir().unwrap_or_else(std::env::temp_dir); - cache - .join("github-copilot-sdk") - .join("cli") - .join(sanitize_version(version)) - } -} - -/// 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, archive: &[u8]) { - std::fs::write(out.join("copilot_cli.archive"), archive) - .expect("failed to write copilot_cli.archive"); - - let generated = r#"// Auto-generated by github-copilot-sdk build.rs. Do not edit. -pub(super) static CLI_ARCHIVE: &[u8] = include_bytes!("copilot_cli.archive"); -"#; - - std::fs::write(out.join("bundled_cli.rs"), generated).expect("failed to write bundled_cli.rs"); -} - -/// Resolve the CLI version and the expected SHA-256 hash for the current -/// target's archive. Picks one of two sources in order. Panics with a clear -/// error if neither is available. -fn resolve_version_and_hash(asset_name: &str) -> (String, String) { - 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, - // vendored-slot consumer). Combined version + per-asset hashes. - 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())); - return parse_snapshot(&contents, asset_name) - .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) - .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); - return (version, hash); - } - - panic!( - "Could not resolve the Copilot CLI version.\n\ - Tried:\n\ - - {} (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.", - snapshot.display(), - lockfile.display(), - ); -} - -/// Parse the `cli-version.txt` snapshot file. Format is one `key=value` per -/// line. The first non-comment line is `version=X.Y.Z`; subsequent lines map -/// asset filename to hex SHA-256. Blank lines and lines starting with `#` -/// are skipped. -fn parse_snapshot(contents: &str, asset_name: &str) -> Result<(String, String), String> { - let mut version: 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('#') { - continue; - } - let (key, value) = line - .split_once('=') - .ok_or_else(|| format!("line {}: expected `key=value`, got `{raw}`", line_no + 1))?; - match key.trim() { - "version" => version = Some(value.trim().to_string()), - k if k == asset_name => hash = Some(value.trim().to_string()), - _ => {} - } - } - let version = version.ok_or("missing `version=` line")?; - let hash = hash.ok_or_else(|| format!("missing hash for asset `{asset_name}`"))?; - Ok((version, hash)) -} - -/// Read the `@github/copilot` version from `nodejs/package-lock.json`. -fn read_version_from_package_lock(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() -} - -/// 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) -} - -#[derive(Clone, Copy)] -struct Platform { - asset_name: &'static str, - binary_name: &'static str, -} - -fn target_platform() -> Option { - let os = std::env::var("CARGO_CFG_TARGET_OS").ok()?; - let arch = std::env::var("CARGO_CFG_TARGET_ARCH").ok()?; - - match (os.as_str(), arch.as_str()) { - ("macos", "aarch64") => Some(Platform { - asset_name: "copilot-darwin-arm64.tar.gz", - binary_name: "copilot", - }), - ("macos", "x86_64") => Some(Platform { - asset_name: "copilot-darwin-x64.tar.gz", - binary_name: "copilot", - }), - ("linux", "x86_64") => Some(Platform { - asset_name: "copilot-linux-x64.tar.gz", - binary_name: "copilot", - }), - ("linux", "aarch64") => Some(Platform { - asset_name: "copilot-linux-arm64.tar.gz", - binary_name: "copilot", - }), - ("windows", "x86_64") => Some(Platform { - asset_name: "copilot-win32-x64.zip", - binary_name: "copilot.exe", - }), - ("windows", "aarch64") => Some(Platform { - asset_name: "copilot-win32-arm64.zip", - binary_name: "copilot.exe", - }), - _ => None, - } -} - -/// Write the single binary entry from `archive` to -/// `/` and return the resulting path. -/// Idempotent — returns the existing path if a previous build already -/// populated the target. -/// -/// Uses file-level staging + atomic rename so a concurrent reader during -/// a parallel `cargo build` race never observes a partially-written -/// binary. `fs::rename` for files is atomic on both Unix and Windows -/// (Windows uses `MoveFileExW` with `MOVEFILE_REPLACE_EXISTING`); for -/// directories it is not, which is why we stage at file granularity. -fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> PathBuf { - let final_path = install_dir.join(platform.binary_name); - - // Caller already gated on `final_path.is_file()`; this is a safety - // net for any future caller that forgets. - if final_path.is_file() { - return final_path; - } - - std::fs::create_dir_all(install_dir).unwrap_or_else(|e| { - panic!( - "failed to create install dir {}: {e}", - install_dir.display() - ) - }); - - let bytes = extract_binary_bytes(archive, platform); - - // Staging file is a sibling of the final binary so the rename stays - // on the same filesystem (cross-fs rename is not atomic). PID + nanos - // disambiguate concurrent builds racing on the same cache. - let nanos = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0); - let staging_path = install_dir.join(format!( - ".{}.staging-{}-{nanos}", - platform.binary_name, - std::process::id(), - )); - - { - let mut f = std::fs::File::create(&staging_path).unwrap_or_else(|e| { - let _ = std::fs::remove_file(&staging_path); - panic!( - "failed to create staging file {}: {e}", - staging_path.display() - ); - }); - - if let Err(e) = f.write_all(&bytes) { - let _ = std::fs::remove_file(&staging_path); - panic!( - "failed to write staging file {}: {e}", - staging_path.display() - ); - } - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - if let Err(e) = f.set_permissions(std::fs::Permissions::from_mode(0o755)) { - let _ = std::fs::remove_file(&staging_path); - panic!("failed to chmod {}: {e}", staging_path.display()); - } - } - - // Backdate the staged binary to the Unix epoch before it lands. We emit - // `cargo:rerun-if-changed` on `final_path` (see caller) so a *deleted* - // cache binary forces a re-extract — but cargo stamps the build-script - // `output` reference when the script is spawned, seconds before this - // freshly-downloaded binary is written. A current mtime would therefore - // be *newer* than that reference, so the next identical `cargo` - // invocation would see the watched file as "changed" and pointlessly - // rerun build.rs + recompile the crate + relink every downstream crate. - // Pinning to the epoch keeps the file unambiguously older than any real - // build reference; `rename` preserves mtime (same inode), so it lands - // already-backdated and a no-change rebuild stays a true no-op. The - // deleted-file recovery contract is untouched: a missing file can't be - // stat'd, so cargo still treats it as stale and reruns regardless. - // - // Best-effort: a filesystem that refuses the epoch (e.g. FAT's 1980 floor - // clamps it — still older than any real reference) or rejects the call - // just reverts to the pre-fix redundant-rebuild behaviour, never a broken - // build. - if let Err(e) = f.set_modified(std::time::SystemTime::UNIX_EPOCH) { - println!( - "cargo:warning=Could not backdate {} (a redundant rebuild may occur): {e}", - staging_path.display() - ); - } - } - - // Atomic file-replace on both Unix and Windows. If a concurrent build - // already produced the same file the rename overwrites it; the bytes - // are SHA-verified-identical so replacement is safe. - if let Err(e) = std::fs::rename(&staging_path, &final_path) { - let _ = std::fs::remove_file(&staging_path); - panic!( - "failed to rename {} -> {}: {e}", - staging_path.display(), - final_path.display() - ); - } - - // Surface where the binary landed so contributors can find it. Quiet - // on the hot path: the caller's `is_file()` short-circuit (and the - // safety net at the top of this function) means this only fires on a - // true cache miss. - println!( - "cargo:warning=Extracted Copilot CLI to {}", - final_path.display() - ); - - final_path -} - -/// Replace characters outside `[a-zA-Z0-9._-]` with `_` so the version -/// string is always safe to use as a path component. Kept in sync with -/// `embeddedcli::sanitize_version` and `resolve::sanitize_version` so all -/// three resolve to the same cache directory for any given version. -fn sanitize_version(version: &str) -> String { - version - .chars() - .map(|c| match c { - 'a'..='z' | 'A'..='Z' | '0'..='9' | '.' | '-' | '_' => c, - _ => '_', - }) - .collect() -} - -/// Extract the single `binary_name` entry from the release archive. Reused -/// between embed mode's `verify_binary_present_in_archive` and the -/// `extract_to_cache` path used when `bundled-cli` is off. Panics if the -/// entry isn't found — callers have already invoked -/// `verify_binary_present_in_archive`. -fn extract_binary_bytes(archive: &[u8], platform: Platform) -> Vec { - if platform.asset_name.ends_with(".zip") { - let cursor = std::io::Cursor::new(archive); - let mut zip = zip::ZipArchive::new(cursor) - .unwrap_or_else(|e| panic!("failed to open zip archive: {e}")); - for i in 0..zip.len() { - let mut entry = zip - .by_index(i) - .unwrap_or_else(|e| panic!("failed to read zip entry {i}: {e}")); - let name = entry.name().to_string(); - if name == platform.binary_name || name.ends_with(&format!("/{}", platform.binary_name)) - { - let mut bytes = Vec::with_capacity(entry.size() as usize); - std::io::copy(&mut entry, &mut bytes) - .unwrap_or_else(|e| panic!("failed to read zip entry bytes: {e}")); - return bytes; - } - } - } else { - let gz = flate2::read::GzDecoder::new(archive); - let mut tar = tar::Archive::new(gz); - for entry in tar - .entries() - .unwrap_or_else(|e| panic!("failed to read tar entries: {e}")) - { - let mut entry = entry.unwrap_or_else(|e| panic!("failed to read tar entry: {e}")); - let path = entry - .path() - .unwrap_or_else(|e| panic!("failed to read tar entry path: {e}")); - let name = path.to_string_lossy().into_owned(); - if name == platform.binary_name || name.ends_with(&format!("/{}", platform.binary_name)) - { - let mut bytes = Vec::with_capacity(entry.size() as usize); - entry - .read_to_end(&mut bytes) - .unwrap_or_else(|e| panic!("failed to read tar entry bytes: {e}")); - return bytes; - } - } - } - panic!( - "binary `{}` not found in archive `{}`", - platform.binary_name, platform.asset_name - ); -} - -/// Read a file from the download cache, or download it (with retries) and save -/// 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_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 hex_sha256(&data) == expected_hash => { - // Silent cache hit — nothing to surface. - return data; - } - Ok(_) => { - println!("cargo:warning=Cached archive hash mismatch, re-downloading"); - let _ = std::fs::remove_file(&cached_path); - } - Err(e) => { - println!( - "cargo:warning=Failed to read cache {}, re-downloading: {e}", - cached_path.display() - ); - } - } - } - } - - println!("cargo:warning=Downloading {url}"); - let data = download_with_retry(url); - let actual_hash = hex_sha256(&data); - if actual_hash != expected_hash { - panic!( - "Archive integrity check failed for {url}!\n expected: {expected_hash}\n actual: {actual_hash}\n \ - This could indicate a corrupted download or a supply-chain attack." - ); - } - - if let Some(dir) = cache_dir { - if let Err(e) = std::fs::create_dir_all(dir) { - println!( - "cargo:warning=Failed to create cache directory {}: {e}", - dir.display() - ); - } else { - let cached_path = dir.join(cache_key); - println!("cargo:warning=Caching archive at {}", cached_path.display()); - if let Err(e) = std::fs::write(&cached_path, &data) { - println!( - "cargo:warning=Failed to write cache file {}: {e}", - cached_path.display() - ); - } - } - } - - data -} - -/// Maximum number of HTTP attempts (one initial + this many retries on transient errors). -const MAX_RETRIES: u32 = 3; - -/// Download `url` with bounded retries on transient network errors. Backoff is -/// exponential starting at 1s. 4xx responses fail fast; 5xx and connect/read -/// errors are retried. -fn download_with_retry(url: &str) -> Vec { - let mut attempt = 0u32; - loop { - attempt += 1; - match try_download(url) { - Ok(bytes) => return bytes, - Err(err) if err.transient && attempt <= MAX_RETRIES => { - let backoff = Duration::from_secs(1u64 << (attempt - 1)); - println!( - "cargo:warning=Transient download failure for {url} (attempt {attempt}/{}): {} — retrying in {}s", - MAX_RETRIES + 1, - err.message, - backoff.as_secs(), - ); - std::thread::sleep(backoff); - } - Err(err) => panic!("Failed to download {url}: {}", err.message), - } - } -} - -struct DownloadError { - message: String, - transient: bool, -} - -fn try_download(url: &str) -> Result, DownloadError> { - let agent = ureq::AgentBuilder::new() - .timeout_connect(Duration::from_secs(30)) - .timeout_read(Duration::from_secs(120)) - .build(); - - match agent.get(url).call() { - Ok(response) => { - let mut bytes = Vec::new(); - response - .into_reader() - .read_to_end(&mut bytes) - .map_err(|e| DownloadError { - message: format!("read error: {e}"), - transient: true, - })?; - Ok(bytes) - } - // 5xx — server-side, treat as transient. - Err(ureq::Error::Status(code, response)) if (500..600).contains(&code) => { - Err(DownloadError { - message: format!("HTTP {code} {}", response.status_text()), - transient: true, - }) - } - // 4xx — client-side, fail fast. - Err(ureq::Error::Status(code, response)) => Err(DownloadError { - message: format!("HTTP {code} {}", response.status_text()), - transient: false, - }), - // Transport-layer (DNS, connect, TLS, read timeout) — treat as transient. - Err(ureq::Error::Transport(t)) => Err(DownloadError { - message: format!("transport error: {t}"), - transient: true, - }), - } -} - -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); - hasher.finalize().into() -} - -/// Walks the downloaded archive at build time to confirm an entry matching -/// `binary_name` exists. Panics with a clear message if not — defends against -/// silent breakage if the upstream archive layout ever changes. -fn verify_binary_present_in_archive(archive: &[u8], binary_name: &str, asset_name: &str) { - let found = if asset_name.ends_with(".zip") { - archive_contains_zip_entry(archive, binary_name) - } else { - archive_contains_tar_entry(archive, binary_name) - }; - if !found { - panic!( - "Copilot CLI archive `{asset_name}` does not contain an entry named `{binary_name}`. \ - The upstream archive layout may have changed; runtime extraction would fail. \ - Update `verify_binary_present_in_archive` in build.rs and the matching `extract_binary` in src/embeddedcli.rs." - ); - } -} - -fn archive_contains_tar_entry(targz: &[u8], binary_name: &str) -> bool { - let gz = flate2::read::GzDecoder::new(targz); - let mut archive = tar::Archive::new(gz); - let Ok(entries) = archive.entries() else { - return false; - }; - for entry in entries.flatten() { - let Ok(path) = entry.path() else { - continue; - }; - let name = path.to_string_lossy(); - if name == binary_name || name.ends_with(&format!("/{binary_name}")) { - return true; - } - } - false -} - -fn archive_contains_zip_entry(zip_bytes: &[u8], binary_name: &str) -> bool { - let cursor = std::io::Cursor::new(zip_bytes); - let Ok(mut archive) = zip::ZipArchive::new(cursor) else { - return false; - }; - for i in 0..archive.len() { - let Ok(entry) = archive.by_index(i) else { - continue; - }; - let name = entry.name(); - if name == binary_name || name.ends_with(&format!("/{binary_name}")) { - return true; - } - } - false -} - -fn hex_sha256(data: &[u8]) -> String { - sha256(data).iter().map(|b| format!("{b:02x}")).collect() + implementation::main(); } diff --git a/rust/build/in_process.rs b/rust/build/in_process.rs new file mode 100644 index 0000000000..5826fbfa76 --- /dev/null +++ b/rust/build/in_process.rs @@ -0,0 +1,726 @@ +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::time::Duration; + +use base64::Engine; +use sha2::Digest; + +pub(crate) fn main() { + println!("cargo:rerun-if-env-changed=DOCS_RS"); + println!("cargo:rerun-if-env-changed=COPILOT_SKIP_CLI_DOWNLOAD"); + println!("cargo:rerun-if-env-changed=COPILOT_CLI_EXTRACT_DIR"); + 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-in-process.txt"); + + // Only declare the lockfile rerun when the lockfile 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. + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"); + let lockfile = Path::new(&manifest_dir) + .join("..") + .join("nodejs") + .join("package-lock.json"); + if lockfile.is_file() { + println!("cargo:rerun-if-changed={}", lockfile.display()); + } + + // Hard opt-out: disable the entire download / bundle / cache mechanism + // in one step. For consumers who always supply the CLI via + // `CliProgram::Path` or `COPILOT_CLI_PATH` and don't want build.rs to + // touch the network (offline builds, locked-down CI, etc.). Works + // regardless of the `bundled-cli` cargo feature state — with neither + // `has_bundled_cli` nor `has_extracted_cli` emitted, runtime resolution + // falls straight through to `Error::BinaryNotFound` unless an explicit + // path source resolves first. + if std::env::var_os("COPILOT_SKIP_CLI_DOWNLOAD").is_some() { + println!( + "cargo:warning=COPILOT_SKIP_CLI_DOWNLOAD is set — skipping CLI download/bundle/cache" + ); + return; + } + + // docs.rs builds in a sandboxed environment without network access. + // Skip the CLI download so documentation can be generated successfully. + if std::env::var_os("DOCS_RS").is_some() { + println!("cargo:warning=DOCS_RS is set — skipping CLI download/bundle/cache"); + return; + } + + let Some(platform) = target_platform() else { + println!("cargo:warning=Unsupported target platform for Copilot CLI bundling — skipping"); + return; + }; + + 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: + // 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); + + // Bake the version into the crate regardless of mode. This is the + // single source of truth for "what CLI version did build.rs target", + // consumed by both the embed-mode path computation in embeddedcli.rs + // and the runtime path computation in resolve.rs (when `bundled-cli` + // is off). It's a small, machine-independent datum: no absolute + // paths, no username/home leakage, so sccache / cross-machine + // `target/` reuse stays cache-coherent. + println!("cargo:rustc-env=COPILOT_SDK_CLI_VERSION={version}"); + + let archive_name = format!("{}-{version}.tgz", platform.package_name); + let download_url = format!( + "https://registry.npmjs.org/@github/{}/-/{}", + platform.package_name, archive_name + ); + let cache_dir = std::env::var("BUNDLED_CLI_CACHE_DIR") + .ok() + .map(std::path::PathBuf::from); + + let cache_key = format!("v{version}-{archive_name}"); + 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_binary_present_in_archive(&archive, platform.binary_name, &archive_name); + emit_embedded(out, &archive, platform, include_runtime); + println!("cargo:rustc-cfg=has_bundled_cli"); + } else { + // With `bundled-cli` off the extracted binary *is* the cache. + // Skip the upstream download entirely when it already exists at + // the expected path. No two separate caches. + // + // Runtime resolution (see `src/resolve.rs::extracted_cli_path`) + // recomputes this same path from `COPILOT_SDK_CLI_VERSION` + the + // OS-derived binary name + optional `COPILOT_CLI_EXTRACT_DIR`, + // so we don't bake an absolute path into the crate. + let install_dir = extracted_install_dir(&version); + let final_path = install_dir.join(platform.binary_name); + + // Invalidate build.rs whenever the cached binary disappears (cache GC, + // manual rm, OS reset, switching extract dir). Without this, cargo + // replays the saved `has_extracted_cli` cfg from its build-script + // output cache even when the file is gone, and runtime resolution + // fails with BinaryNotFound. + println!("cargo:rerun-if-changed={}", final_path.display()); + + if !final_path.is_file() { + let archive = + cached_download(&download_url, &cache_key, &expected_integrity, &cache_dir); + verify_binary_present_in_archive(&archive, platform.binary_name, &archive_name); + extract_to_cache(&archive, &install_dir, platform); + } + + // Re-check after potential download+extract above; not an `else` + // because we need to verify the extraction actually produced the file. + if final_path.is_file() { + println!("cargo:rustc-cfg=has_extracted_cli"); + } + } +} + +/// Install directory used when `bundled-cli` is off. Mirrors the runtime +/// convention in `src/resolve.rs::extracted_cli_path`: both sides MUST +/// compute the same path from the same inputs, otherwise the runtime +/// resolver won't find what build.rs extracted. +/// +/// If `COPILOT_CLI_EXTRACT_DIR` is set the binary lives directly under +/// that directory (no per-version subdir) — useful for vendored slots and +/// for `.cargo/config.toml [env]`-style pinning that's symmetric between +/// build-time write and runtime read. Otherwise the binary lives under +/// `/github-copilot-sdk/cli//`. +fn extracted_install_dir(version: &str) -> PathBuf { + if let Some(custom) = std::env::var_os("COPILOT_CLI_EXTRACT_DIR") { + PathBuf::from(custom) + } else { + let cache = dirs::cache_dir().unwrap_or_else(std::env::temp_dir); + cache + .join("github-copilot-sdk") + .join("cli") + .join(sanitize_version(version)) + } +} + +/// 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) + .expect("failed to write copilot_cli.archive"); + + let generated = r#"// Auto-generated by github-copilot-sdk build.rs. Do not edit. +pub(super) static CLI_ARCHIVE: &[u8] = include_bytes!("copilot_cli.archive"); +"#; + + 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 { + let encoder = flate2::GzBuilder::new() + .mtime(0) + .write(Vec::new(), flate2::Compression::default()); + let mut archive = tar::Builder::new(encoder); + append_archive_file( + &mut archive, + platform.binary_name, + &extract_binary_bytes(package, platform), + 0o755, + ); + if include_runtime { + let runtime = extract_runtime_library_bytes(package).unwrap_or_else(|| { + panic!( + "package `{}` does not contain the native runtime library required by the `bundled-in-process` feature", + platform.package_name + ) + }); + append_archive_file( + &mut archive, + platform.runtime_library_name(), + &runtime, + 0o644, + ); + } + let encoder = archive + .into_inner() + .expect("failed to finish minimal embedded CLI archive"); + encoder + .finish() + .expect("failed to compress minimal embedded CLI archive") +} + +fn append_archive_file( + archive: &mut tar::Builder, + path: &str, + bytes: &[u8], + mode: u32, +) { + let mut header = tar::Header::new_gnu(); + header.set_size(bytes.len() as u64); + header.set_mode(mode); + header.set_uid(0); + header.set_gid(0); + header.set_mtime(0); + header.set_cksum(); + archive + .append_data(&mut header, path, bytes) + .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) { + 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, + // vendored-slot consumer). Combined version + per-asset hashes. + let snapshot = Path::new(&manifest_dir).join("cli-version-in-process.txt"); + 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) + .unwrap_or_else(|e| panic!("invalid {}: {e}", snapshot.display())); + } + + // 2. Lockfile fallback (contributor build inside github/copilot-sdk). + let lockfile = 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); + } + + panic!( + "Could not resolve the Copilot CLI version.\n\ + Tried:\n\ + - {} (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.", + snapshot.display(), + lockfile.display(), + ); +} + +/// 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 `#` +/// are skipped. +fn parse_snapshot(contents: &str, package_name: &str) -> Result<(String, String), String> { + let mut version: Option = None; + let mut integrity: Option = None; + for (line_no, raw) in contents.lines().enumerate() { + let line = raw.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let Some((key, value)) = line.split_once('=') else { + return Err(format!( + "line {}: expected `key=value`, got `{raw}`", + line_no + 1 + )); + }; + match key.trim() { + "version" => version = Some(value.trim().to_string()), + k if k == package_name => integrity = 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)) +} + +fn read_version_and_integrity_from_package_lock( + path: &Path, + package_name: &str, +) -> (String, 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) + .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"] + .as_str() + .unwrap_or_else(|| panic!("{platform_key} has no integrity in {}", path.display())); + (version.to_string(), integrity.to_string()) +} + +#[derive(Clone, Copy)] +struct Platform { + package_name: &'static str, + binary_name: &'static str, +} + +impl Platform { + fn runtime_library_name(&self) -> &'static str { + if self.package_name.contains("win32") { + "copilot_runtime.dll" + } else if self.package_name.contains("darwin") { + "libcopilot_runtime.dylib" + } else { + "libcopilot_runtime.so" + } + } +} + +fn target_platform() -> Option { + let os = std::env::var("CARGO_CFG_TARGET_OS").ok()?; + let arch = std::env::var("CARGO_CFG_TARGET_ARCH").ok()?; + let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default(); + + match (os.as_str(), arch.as_str(), target_env.as_str()) { + ("macos", "aarch64", _) => Some(Platform { + package_name: "copilot-darwin-arm64", + binary_name: "copilot", + }), + ("macos", "x86_64", _) => Some(Platform { + package_name: "copilot-darwin-x64", + binary_name: "copilot", + }), + ("linux", "x86_64", "musl") => Some(Platform { + package_name: "copilot-linuxmusl-x64", + binary_name: "copilot", + }), + ("linux", "aarch64", "musl") => Some(Platform { + package_name: "copilot-linuxmusl-arm64", + binary_name: "copilot", + }), + ("linux", "x86_64", _) => Some(Platform { + package_name: "copilot-linux-x64", + binary_name: "copilot", + }), + ("linux", "aarch64", _) => Some(Platform { + package_name: "copilot-linux-arm64", + binary_name: "copilot", + }), + ("windows", "x86_64", _) => Some(Platform { + package_name: "copilot-win32-x64", + binary_name: "copilot.exe", + }), + ("windows", "aarch64", _) => Some(Platform { + package_name: "copilot-win32-arm64", + binary_name: "copilot.exe", + }), + _ => None, + } +} + +/// Write the single binary entry from `archive` to +/// `/` and return the resulting path. +/// Idempotent — returns the existing path if a previous build already +/// populated the target. +/// +/// Uses file-level staging + atomic rename so a concurrent reader during +/// a parallel `cargo build` race never observes a partially-written +/// binary. `fs::rename` for files is atomic on both Unix and Windows +/// (Windows uses `MoveFileExW` with `MOVEFILE_REPLACE_EXISTING`); for +/// directories it is not, which is why we stage at file granularity. +fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> PathBuf { + let final_path = install_dir.join(platform.binary_name); + + // Caller already gated on `final_path.is_file()`; this is a safety + // net for any future caller that forgets. + if final_path.is_file() { + return final_path; + } + + std::fs::create_dir_all(install_dir).unwrap_or_else(|e| { + panic!( + "failed to create install dir {}: {e}", + install_dir.display() + ) + }); + + let bytes = extract_binary_bytes(archive, platform); + + // Staging file is a sibling of the final binary so the rename stays + // on the same filesystem (cross-fs rename is not atomic). PID + nanos + // disambiguate concurrent builds racing on the same cache. + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or(0); + let staging_path = install_dir.join(format!( + ".{}.staging-{}-{nanos}", + platform.binary_name, + std::process::id(), + )); + + { + let mut f = std::fs::File::create(&staging_path).unwrap_or_else(|e| { + let _ = std::fs::remove_file(&staging_path); + panic!( + "failed to create staging file {}: {e}", + staging_path.display() + ); + }); + + if let Err(e) = f.write_all(&bytes) { + let _ = std::fs::remove_file(&staging_path); + panic!( + "failed to write staging file {}: {e}", + staging_path.display() + ); + } + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if let Err(e) = f.set_permissions(std::fs::Permissions::from_mode(0o755)) { + let _ = std::fs::remove_file(&staging_path); + panic!("failed to chmod {}: {e}", staging_path.display()); + } + } + + // Backdate the staged binary to the Unix epoch before it lands. We emit + // `cargo:rerun-if-changed` on `final_path` (see caller) so a *deleted* + // cache binary forces a re-extract — but cargo stamps the build-script + // `output` reference when the script is spawned, seconds before this + // freshly-downloaded binary is written. A current mtime would therefore + // be *newer* than that reference, so the next identical `cargo` + // invocation would see the watched file as "changed" and pointlessly + // rerun build.rs + recompile the crate + relink every downstream crate. + // Pinning to the epoch keeps the file unambiguously older than any real + // build reference; `rename` preserves mtime (same inode), so it lands + // already-backdated and a no-change rebuild stays a true no-op. The + // deleted-file recovery contract is untouched: a missing file can't be + // stat'd, so cargo still treats it as stale and reruns regardless. + // + // Best-effort: a filesystem that refuses the epoch (e.g. FAT's 1980 floor + // clamps it — still older than any real reference) or rejects the call + // just reverts to the pre-fix redundant-rebuild behaviour, never a broken + // build. + if let Err(e) = f.set_modified(std::time::SystemTime::UNIX_EPOCH) { + println!( + "cargo:warning=Could not backdate {} (a redundant rebuild may occur): {e}", + staging_path.display() + ); + } + } + + // Atomic file-replace on both Unix and Windows. If a concurrent build + // already produced the same file the rename overwrites it; the bytes + // are integrity-verified-identical so replacement is safe. + if let Err(e) = std::fs::rename(&staging_path, &final_path) { + let _ = std::fs::remove_file(&staging_path); + panic!( + "failed to rename {} -> {}: {e}", + staging_path.display(), + final_path.display() + ); + } + + // Surface where the binary landed so contributors can find it. Quiet + // on the hot path: the caller's `is_file()` short-circuit (and the + // safety net at the top of this function) means this only fires on a + // true cache miss. + println!( + "cargo:warning=Extracted Copilot CLI to {}", + final_path.display() + ); + + final_path +} + +fn extract_runtime_library_bytes(archive: &[u8]) -> Option> { + let gz = flate2::read::GzDecoder::new(archive); + let mut tar = tar::Archive::new(gz); + for entry in tar.entries().ok()? { + let mut entry = entry.ok()?; + let name = entry.path().ok()?.to_string_lossy().into_owned(); + if name == "runtime.node" || name.ends_with("/runtime.node") { + let mut bytes = Vec::with_capacity(entry.size() as usize); + entry.read_to_end(&mut bytes).ok()?; + return Some(bytes); + } + } + None +} + +/// Replace characters outside `[a-zA-Z0-9._-]` with `_` so the version +/// string is always safe to use as a path component. Kept in sync with +/// `embeddedcli::sanitize_version` and `resolve::sanitize_version` so all +/// three resolve to the same cache directory for any given version. +fn sanitize_version(version: &str) -> String { + version + .chars() + .map(|c| match c { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '.' | '-' | '_' => c, + _ => '_', + }) + .collect() +} + +/// Extract the single `binary_name` entry from the npm package archive. Reused +/// between embed mode's `verify_binary_present_in_archive` and the +/// `extract_to_cache` path used when `bundled-cli` is off. Panics if the +/// entry isn't found — callers have already invoked +/// `verify_binary_present_in_archive`. +fn extract_binary_bytes(archive: &[u8], platform: Platform) -> Vec { + let gz = flate2::read::GzDecoder::new(archive); + let mut tar = tar::Archive::new(gz); + for entry in tar + .entries() + .unwrap_or_else(|e| panic!("failed to read tar entries: {e}")) + { + let mut entry = entry.unwrap_or_else(|e| panic!("failed to read tar entry: {e}")); + let path = entry + .path() + .unwrap_or_else(|e| panic!("failed to read tar entry path: {e}")); + let name = path.to_string_lossy().into_owned(); + if name == platform.binary_name || name.ends_with(&format!("/{}", platform.binary_name)) { + let mut bytes = Vec::with_capacity(entry.size() as usize); + entry + .read_to_end(&mut bytes) + .unwrap_or_else(|e| panic!("failed to read tar entry bytes: {e}")); + return bytes; + } + } + panic!( + "binary `{}` not found in package `{}`", + platform.binary_name, platform.package_name + ); +} + +/// 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 +/// 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, + 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) => { + // Silent cache hit — nothing to surface. + return data; + } + Ok(_) => { + println!("cargo:warning=Cached archive hash mismatch, re-downloading"); + let _ = std::fs::remove_file(&cached_path); + } + Err(e) => { + println!( + "cargo:warning=Failed to read cache {}, re-downloading: {e}", + cached_path.display() + ); + } + } + } + } + + println!("cargo:warning=Downloading {url}"); + let data = download_with_retry(url); + if !verify_integrity(&data, expected_integrity) { + panic!( + "Archive integrity check failed for {url}!\n expected: {expected_integrity}\n \ + This could indicate a corrupted download or a supply-chain attack." + ); + } + + if let Some(dir) = cache_dir { + if let Err(e) = std::fs::create_dir_all(dir) { + println!( + "cargo:warning=Failed to create cache directory {}: {e}", + dir.display() + ); + } else { + let cached_path = dir.join(cache_key); + println!("cargo:warning=Caching archive at {}", cached_path.display()); + if let Err(e) = std::fs::write(&cached_path, &data) { + println!( + "cargo:warning=Failed to write cache file {}: {e}", + cached_path.display() + ); + } + } + } + + data +} + +/// Maximum number of HTTP attempts (one initial + this many retries on transient errors). +const MAX_RETRIES: u32 = 3; + +/// Download `url` with bounded retries on transient network errors. Backoff is +/// exponential starting at 1s. 4xx responses fail fast; 5xx and connect/read +/// errors are retried. +fn download_with_retry(url: &str) -> Vec { + let mut attempt = 0u32; + loop { + attempt += 1; + match try_download(url) { + Ok(bytes) => return bytes, + Err(err) if err.transient && attempt <= MAX_RETRIES => { + let backoff = Duration::from_secs(1u64 << (attempt - 1)); + println!( + "cargo:warning=Transient download failure for {url} (attempt {attempt}/{}): {} — retrying in {}s", + MAX_RETRIES + 1, + err.message, + backoff.as_secs(), + ); + std::thread::sleep(backoff); + } + Err(err) => panic!("Failed to download {url}: {}", err.message), + } + } +} + +struct DownloadError { + message: String, + transient: bool, +} + +fn try_download(url: &str) -> Result, DownloadError> { + let connector = native_tls::TlsConnector::new().map_err(|e| DownloadError { + message: format!("native-tls init error: {e}"), + transient: false, + })?; + let agent = ureq::AgentBuilder::new() + .tls_connector(std::sync::Arc::new(connector)) + .timeout_connect(Duration::from_secs(30)) + .timeout_read(Duration::from_secs(120)) + .build(); + + match agent.get(url).call() { + Ok(response) => { + let mut bytes = Vec::new(); + response + .into_reader() + .read_to_end(&mut bytes) + .map_err(|e| DownloadError { + message: format!("read error: {e}"), + transient: true, + })?; + Ok(bytes) + } + // 5xx — server-side, treat as transient. + Err(ureq::Error::Status(code, response)) if (500..600).contains(&code) => { + Err(DownloadError { + message: format!("HTTP {code} {}", response.status_text()), + transient: true, + }) + } + // 4xx — client-side, fail fast. + Err(ureq::Error::Status(code, response)) => Err(DownloadError { + message: format!("HTTP {code} {}", response.status_text()), + transient: false, + }), + // Transport-layer (DNS, connect, TLS, read timeout) — treat as transient. + Err(ureq::Error::Transport(t)) => Err(DownloadError { + message: format!("transport error: {t}"), + transient: true, + }), + } +} + +/// Walks the downloaded archive at build time to confirm an entry matching +/// `binary_name` exists. Panics with a clear message if not. +fn verify_binary_present_in_archive(archive: &[u8], binary_name: &str, package_name: &str) { + let found = archive_contains_tar_entry(archive, binary_name); + if !found { + panic!( + "Copilot CLI package `{package_name}` does not contain an entry named `{binary_name}`. \ + The package layout may have changed; runtime extraction would fail. \ + Update `verify_binary_present_in_archive` in build.rs and the matching `extract_binary` in src/embeddedcli.rs." + ); + } +} + +fn archive_contains_tar_entry(targz: &[u8], binary_name: &str) -> bool { + let gz = flate2::read::GzDecoder::new(targz); + let mut archive = tar::Archive::new(gz); + let Ok(entries) = archive.entries() else { + return false; + }; + for entry in entries.flatten() { + let Ok(path) = entry.path() else { + continue; + }; + let name = path.to_string_lossy(); + if name == binary_name || name.ends_with(&format!("/{binary_name}")) { + return true; + } + } + false +} + +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; + }; + let mut hasher = sha2::Sha512::new(); + hasher.update(data); + hasher.finalize().as_slice() == expected +} diff --git a/rust/build/out_of_process.rs b/rust/build/out_of_process.rs new file mode 100644 index 0000000000..b8cd3acc3f --- /dev/null +++ b/rust/build/out_of_process.rs @@ -0,0 +1,717 @@ +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::time::Duration; + +use sha2::Digest; + +pub(crate) fn main() { + println!("cargo:rerun-if-env-changed=DOCS_RS"); + println!("cargo:rerun-if-env-changed=COPILOT_SKIP_CLI_DOWNLOAD"); + println!("cargo:rerun-if-env-changed=COPILOT_CLI_EXTRACT_DIR"); + 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"); + + // Only declare the lockfile rerun when the lockfile 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.txt` is canonical. + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"); + let lockfile = Path::new(&manifest_dir) + .join("..") + .join("nodejs") + .join("package-lock.json"); + if lockfile.is_file() { + println!("cargo:rerun-if-changed={}", lockfile.display()); + } + + // Hard opt-out: disable the entire download / bundle / cache mechanism + // in one step. For consumers who always supply the CLI via + // `CliProgram::Path` or `COPILOT_CLI_PATH` and don't want build.rs to + // touch the network (offline builds, locked-down CI, etc.). Works + // regardless of the `bundled-cli` cargo feature state — with neither + // `has_bundled_cli` nor `has_extracted_cli` emitted, runtime resolution + // falls straight through to `Error::BinaryNotFound` unless an explicit + // path source resolves first. + if std::env::var_os("COPILOT_SKIP_CLI_DOWNLOAD").is_some() { + println!( + "cargo:warning=COPILOT_SKIP_CLI_DOWNLOAD is set — skipping CLI download/bundle/cache" + ); + return; + } + + // docs.rs builds in a sandboxed environment without network access. + // Skip the CLI download so documentation can be generated successfully. + if std::env::var_os("DOCS_RS").is_some() { + println!("cargo:warning=DOCS_RS is set — skipping CLI download/bundle/cache"); + return; + } + + let Some(platform) = target_platform() else { + println!("cargo:warning=Unsupported target platform for Copilot CLI bundling — skipping"); + return; + }; + + let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR is always set by cargo"); + let out = Path::new(&out_dir); + + // 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. + let (version, expected_hash) = resolve_version_and_hash(platform.asset_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", + // consumed by both the embed-mode path computation in embeddedcli.rs + // and the runtime path computation in resolve.rs (when `bundled-cli` + // is off). It's a small, machine-independent datum: no absolute + // paths, no username/home leakage, so sccache / cross-machine + // `target/` reuse stays cache-coherent. + println!("cargo:rustc-env=COPILOT_SDK_CLI_VERSION={version}"); + + let base_url = format!("https://github.com/github/copilot-cli/releases/download/v{version}"); + let cache_dir = std::env::var("BUNDLED_CLI_CACHE_DIR") + .ok() + .map(std::path::PathBuf::from); + + // Versioned cache key since copilot asset names don't include the version. + let cache_key = format!("v{version}-{}", platform.asset_name); + + if std::env::var_os("CARGO_FEATURE_BUNDLED_CLI").is_some() { + // Embed mode: we need the archive bytes to bake into the rlib, so + // always run the download (cache hit short-circuits inside + // `cached_download`). + let archive = cached_download( + &format!("{base_url}/{}", platform.asset_name), + &cache_key, + &expected_hash, + &cache_dir, + ); + verify_binary_present_in_archive(&archive, platform.binary_name, platform.asset_name); + emit_embedded(out, &archive); + println!("cargo:rustc-cfg=has_bundled_cli"); + } else { + // With `bundled-cli` off the extracted binary *is* the cache. + // Skip the upstream download entirely when it already exists at + // the expected path. No two separate caches. + // + // Runtime resolution (see `src/resolve.rs::extracted_cli_path`) + // recomputes this same path from `COPILOT_SDK_CLI_VERSION` + the + // OS-derived binary name + optional `COPILOT_CLI_EXTRACT_DIR`, + // so we don't bake an absolute path into the crate. + let install_dir = extracted_install_dir(&version); + let final_path = install_dir.join(platform.binary_name); + + // Invalidate build.rs whenever the cached binary disappears (cache GC, + // manual rm, OS reset, switching extract dir). Without this, cargo + // replays the saved `has_extracted_cli` cfg from its build-script + // output cache even when the file is gone, and runtime resolution + // fails with BinaryNotFound. + println!("cargo:rerun-if-changed={}", final_path.display()); + + if !final_path.is_file() { + let archive = cached_download( + &format!("{base_url}/{}", platform.asset_name), + &cache_key, + &expected_hash, + &cache_dir, + ); + verify_binary_present_in_archive(&archive, platform.binary_name, platform.asset_name); + extract_to_cache(&archive, &install_dir, platform); + } + + // Re-check after potential download+extract above; not an `else` + // because we need to verify the extraction actually produced the file. + if final_path.is_file() { + println!("cargo:rustc-cfg=has_extracted_cli"); + } + } +} + +/// Install directory used when `bundled-cli` is off. Mirrors the runtime +/// convention in `src/resolve.rs::extracted_cli_path`: both sides MUST +/// compute the same path from the same inputs, otherwise the runtime +/// resolver won't find what build.rs extracted. +/// +/// If `COPILOT_CLI_EXTRACT_DIR` is set the binary lives directly under +/// that directory (no per-version subdir) — useful for vendored slots and +/// for `.cargo/config.toml [env]`-style pinning that's symmetric between +/// build-time write and runtime read. Otherwise the binary lives under +/// `/github-copilot-sdk/cli//`. +fn extracted_install_dir(version: &str) -> PathBuf { + if let Some(custom) = std::env::var_os("COPILOT_CLI_EXTRACT_DIR") { + PathBuf::from(custom) + } else { + let cache = dirs::cache_dir().unwrap_or_else(std::env::temp_dir); + cache + .join("github-copilot-sdk") + .join("cli") + .join(sanitize_version(version)) + } +} + +/// 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, archive: &[u8]) { + std::fs::write(out.join("copilot_cli.archive"), archive) + .expect("failed to write copilot_cli.archive"); + + let generated = r#"// Auto-generated by github-copilot-sdk build.rs. Do not edit. +pub(super) static CLI_ARCHIVE: &[u8] = include_bytes!("copilot_cli.archive"); +"#; + + std::fs::write(out.join("bundled_cli.rs"), generated).expect("failed to write bundled_cli.rs"); +} + +/// Resolve the CLI version and the expected SHA-256 hash for the current +/// target's archive. Picks one of two sources in order. Panics with a clear +/// error if neither is available. +fn resolve_version_and_hash(asset_name: &str) -> (String, String) { + 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, + // vendored-slot consumer). Combined version + per-asset hashes. + 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())); + return parse_snapshot(&contents, asset_name) + .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) + .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); + return (version, hash); + } + + panic!( + "Could not resolve the Copilot CLI version.\n\ + Tried:\n\ + - {} (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.", + snapshot.display(), + lockfile.display(), + ); +} + +/// Parse the `cli-version.txt` snapshot file. Format is one `key=value` per +/// line. The first non-comment line is `version=X.Y.Z`; subsequent lines map +/// asset filename to hex SHA-256. Blank lines and lines starting with `#` +/// are skipped. +fn parse_snapshot(contents: &str, asset_name: &str) -> Result<(String, String), String> { + let mut version: 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('#') { + continue; + } + let Some((key, value)) = line.split_once('=') else { + return Err(format!( + "line {}: expected `key=value`, got `{raw}`", + line_no + 1 + )); + }; + match key.trim() { + "version" => version = Some(value.trim().to_string()), + k if k == asset_name => hash = Some(value.trim().to_string()), + _ => {} + } + } + let version = version.ok_or("missing `version=` line")?; + let hash = hash.ok_or_else(|| format!("missing hash for asset `{asset_name}`"))?; + Ok((version, hash)) +} + +/// Read the `@github/copilot` version from `nodejs/package-lock.json`. +fn read_version_from_package_lock(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() +} + +/// 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) +} + +#[derive(Clone, Copy)] +struct Platform { + asset_name: &'static str, + binary_name: &'static str, +} + +fn target_platform() -> Option { + let os = std::env::var("CARGO_CFG_TARGET_OS").ok()?; + let arch = std::env::var("CARGO_CFG_TARGET_ARCH").ok()?; + + match (os.as_str(), arch.as_str()) { + ("macos", "aarch64") => Some(Platform { + asset_name: "copilot-darwin-arm64.tar.gz", + binary_name: "copilot", + }), + ("macos", "x86_64") => Some(Platform { + asset_name: "copilot-darwin-x64.tar.gz", + binary_name: "copilot", + }), + ("linux", "x86_64") => Some(Platform { + asset_name: "copilot-linux-x64.tar.gz", + binary_name: "copilot", + }), + ("linux", "aarch64") => Some(Platform { + asset_name: "copilot-linux-arm64.tar.gz", + binary_name: "copilot", + }), + ("windows", "x86_64") => Some(Platform { + asset_name: "copilot-win32-x64.zip", + binary_name: "copilot.exe", + }), + ("windows", "aarch64") => Some(Platform { + asset_name: "copilot-win32-arm64.zip", + binary_name: "copilot.exe", + }), + _ => None, + } +} + +/// Write the single binary entry from `archive` to +/// `/` and return the resulting path. +/// Idempotent — returns the existing path if a previous build already +/// populated the target. +/// +/// Uses file-level staging + atomic rename so a concurrent reader during +/// a parallel `cargo build` race never observes a partially-written +/// binary. `fs::rename` for files is atomic on both Unix and Windows +/// (Windows uses `MoveFileExW` with `MOVEFILE_REPLACE_EXISTING`); for +/// directories it is not, which is why we stage at file granularity. +fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> PathBuf { + let final_path = install_dir.join(platform.binary_name); + + // Caller already gated on `final_path.is_file()`; this is a safety + // net for any future caller that forgets. + if final_path.is_file() { + return final_path; + } + + std::fs::create_dir_all(install_dir).unwrap_or_else(|e| { + panic!( + "failed to create install dir {}: {e}", + install_dir.display() + ) + }); + + let bytes = extract_binary_bytes(archive, platform); + + // Staging file is a sibling of the final binary so the rename stays + // on the same filesystem (cross-fs rename is not atomic). PID + nanos + // disambiguate concurrent builds racing on the same cache. + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or(0); + let staging_path = install_dir.join(format!( + ".{}.staging-{}-{nanos}", + platform.binary_name, + std::process::id(), + )); + + { + let mut f = std::fs::File::create(&staging_path).unwrap_or_else(|e| { + let _ = std::fs::remove_file(&staging_path); + panic!( + "failed to create staging file {}: {e}", + staging_path.display() + ); + }); + + if let Err(e) = f.write_all(&bytes) { + let _ = std::fs::remove_file(&staging_path); + panic!( + "failed to write staging file {}: {e}", + staging_path.display() + ); + } + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if let Err(e) = f.set_permissions(std::fs::Permissions::from_mode(0o755)) { + let _ = std::fs::remove_file(&staging_path); + panic!("failed to chmod {}: {e}", staging_path.display()); + } + } + + // Backdate the staged binary to the Unix epoch before it lands. We emit + // `cargo:rerun-if-changed` on `final_path` (see caller) so a *deleted* + // cache binary forces a re-extract — but cargo stamps the build-script + // `output` reference when the script is spawned, seconds before this + // freshly-downloaded binary is written. A current mtime would therefore + // be *newer* than that reference, so the next identical `cargo` + // invocation would see the watched file as "changed" and pointlessly + // rerun build.rs + recompile the crate + relink every downstream crate. + // Pinning to the epoch keeps the file unambiguously older than any real + // build reference; `rename` preserves mtime (same inode), so it lands + // already-backdated and a no-change rebuild stays a true no-op. The + // deleted-file recovery contract is untouched: a missing file can't be + // stat'd, so cargo still treats it as stale and reruns regardless. + // + // Best-effort: a filesystem that refuses the epoch (e.g. FAT's 1980 floor + // clamps it — still older than any real reference) or rejects the call + // just reverts to the pre-fix redundant-rebuild behaviour, never a broken + // build. + if let Err(e) = f.set_modified(std::time::SystemTime::UNIX_EPOCH) { + println!( + "cargo:warning=Could not backdate {} (a redundant rebuild may occur): {e}", + staging_path.display() + ); + } + } + + // Atomic file-replace on both Unix and Windows. If a concurrent build + // already produced the same file the rename overwrites it; the bytes + // are SHA-verified-identical so replacement is safe. + if let Err(e) = std::fs::rename(&staging_path, &final_path) { + let _ = std::fs::remove_file(&staging_path); + panic!( + "failed to rename {} -> {}: {e}", + staging_path.display(), + final_path.display() + ); + } + + // Surface where the binary landed so contributors can find it. Quiet + // on the hot path: the caller's `is_file()` short-circuit (and the + // safety net at the top of this function) means this only fires on a + // true cache miss. + println!( + "cargo:warning=Extracted Copilot CLI to {}", + final_path.display() + ); + + final_path +} + +/// Replace characters outside `[a-zA-Z0-9._-]` with `_` so the version +/// string is always safe to use as a path component. Kept in sync with +/// `embeddedcli::sanitize_version` and `resolve::sanitize_version` so all +/// three resolve to the same cache directory for any given version. +fn sanitize_version(version: &str) -> String { + version + .chars() + .map(|c| match c { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '.' | '-' | '_' => c, + _ => '_', + }) + .collect() +} + +/// Extract the single `binary_name` entry from the release archive. Reused +/// between embed mode's `verify_binary_present_in_archive` and the +/// `extract_to_cache` path used when `bundled-cli` is off. Panics if the +/// entry isn't found — callers have already invoked +/// `verify_binary_present_in_archive`. +fn extract_binary_bytes(archive: &[u8], platform: Platform) -> Vec { + if platform.asset_name.ends_with(".zip") { + let cursor = std::io::Cursor::new(archive); + let mut zip = zip::ZipArchive::new(cursor) + .unwrap_or_else(|e| panic!("failed to open zip archive: {e}")); + for i in 0..zip.len() { + let mut entry = zip + .by_index(i) + .unwrap_or_else(|e| panic!("failed to read zip entry {i}: {e}")); + let name = entry.name().to_string(); + if name == platform.binary_name || name.ends_with(&format!("/{}", platform.binary_name)) + { + let mut bytes = Vec::with_capacity(entry.size() as usize); + std::io::copy(&mut entry, &mut bytes) + .unwrap_or_else(|e| panic!("failed to read zip entry bytes: {e}")); + return bytes; + } + } + } else { + let gz = flate2::read::GzDecoder::new(archive); + let mut tar = tar::Archive::new(gz); + for entry in tar + .entries() + .unwrap_or_else(|e| panic!("failed to read tar entries: {e}")) + { + let mut entry = entry.unwrap_or_else(|e| panic!("failed to read tar entry: {e}")); + let path = entry + .path() + .unwrap_or_else(|e| panic!("failed to read tar entry path: {e}")); + let name = path.to_string_lossy().into_owned(); + if name == platform.binary_name || name.ends_with(&format!("/{}", platform.binary_name)) + { + let mut bytes = Vec::with_capacity(entry.size() as usize); + entry + .read_to_end(&mut bytes) + .unwrap_or_else(|e| panic!("failed to read tar entry bytes: {e}")); + return bytes; + } + } + } + panic!( + "binary `{}` not found in archive `{}`", + platform.binary_name, platform.asset_name + ); +} + +/// Read a file from the download cache, or download it (with retries) and save +/// 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_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 hex_sha256(&data) == expected_hash => { + // Silent cache hit — nothing to surface. + return data; + } + Ok(_) => { + println!("cargo:warning=Cached archive hash mismatch, re-downloading"); + let _ = std::fs::remove_file(&cached_path); + } + Err(e) => { + println!( + "cargo:warning=Failed to read cache {}, re-downloading: {e}", + cached_path.display() + ); + } + } + } + } + + println!("cargo:warning=Downloading {url}"); + let data = download_with_retry(url); + let actual_hash = hex_sha256(&data); + if actual_hash != expected_hash { + panic!( + "Archive integrity check failed for {url}!\n expected: {expected_hash}\n actual: {actual_hash}\n \ + This could indicate a corrupted download or a supply-chain attack." + ); + } + + if let Some(dir) = cache_dir { + if let Err(e) = std::fs::create_dir_all(dir) { + println!( + "cargo:warning=Failed to create cache directory {}: {e}", + dir.display() + ); + } else { + let cached_path = dir.join(cache_key); + println!("cargo:warning=Caching archive at {}", cached_path.display()); + if let Err(e) = std::fs::write(&cached_path, &data) { + println!( + "cargo:warning=Failed to write cache file {}: {e}", + cached_path.display() + ); + } + } + } + + data +} + +/// Maximum number of HTTP attempts (one initial + this many retries on transient errors). +const MAX_RETRIES: u32 = 3; + +/// Download `url` with bounded retries on transient network errors. Backoff is +/// exponential starting at 1s. 4xx responses fail fast; 5xx and connect/read +/// errors are retried. +fn download_with_retry(url: &str) -> Vec { + let mut attempt = 0u32; + loop { + attempt += 1; + match try_download(url) { + Ok(bytes) => return bytes, + Err(err) if err.transient && attempt <= MAX_RETRIES => { + let backoff = Duration::from_secs(1u64 << (attempt - 1)); + println!( + "cargo:warning=Transient download failure for {url} (attempt {attempt}/{}): {} — retrying in {}s", + MAX_RETRIES + 1, + err.message, + backoff.as_secs(), + ); + std::thread::sleep(backoff); + } + Err(err) => panic!("Failed to download {url}: {}", err.message), + } + } +} + +struct DownloadError { + message: String, + transient: bool, +} + +fn try_download(url: &str) -> Result, DownloadError> { + let connector = native_tls::TlsConnector::new().map_err(|e| DownloadError { + message: format!("native-tls init error: {e}"), + transient: false, + })?; + let agent = ureq::AgentBuilder::new() + .tls_connector(std::sync::Arc::new(connector)) + .timeout_connect(Duration::from_secs(30)) + .timeout_read(Duration::from_secs(120)) + .build(); + + match agent.get(url).call() { + Ok(response) => { + let mut bytes = Vec::new(); + response + .into_reader() + .read_to_end(&mut bytes) + .map_err(|e| DownloadError { + message: format!("read error: {e}"), + transient: true, + })?; + Ok(bytes) + } + // 5xx — server-side, treat as transient. + Err(ureq::Error::Status(code, response)) if (500..600).contains(&code) => { + Err(DownloadError { + message: format!("HTTP {code} {}", response.status_text()), + transient: true, + }) + } + // 4xx — client-side, fail fast. + Err(ureq::Error::Status(code, response)) => Err(DownloadError { + message: format!("HTTP {code} {}", response.status_text()), + transient: false, + }), + // Transport-layer (DNS, connect, TLS, read timeout) — treat as transient. + Err(ureq::Error::Transport(t)) => Err(DownloadError { + message: format!("transport error: {t}"), + transient: true, + }), + } +} + +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); + hasher.finalize().into() +} + +/// Walks the downloaded archive at build time to confirm an entry matching +/// `binary_name` exists. Panics with a clear message if not — defends against +/// silent breakage if the upstream archive layout ever changes. +fn verify_binary_present_in_archive(archive: &[u8], binary_name: &str, asset_name: &str) { + let found = if asset_name.ends_with(".zip") { + archive_contains_zip_entry(archive, binary_name) + } else { + archive_contains_tar_entry(archive, binary_name) + }; + if !found { + panic!( + "Copilot CLI archive `{asset_name}` does not contain an entry named `{binary_name}`. \ + The upstream archive layout may have changed; runtime extraction would fail. \ + Update `verify_binary_present_in_archive` in build.rs and the matching `extract_binary` in src/embeddedcli.rs." + ); + } +} + +fn archive_contains_tar_entry(targz: &[u8], binary_name: &str) -> bool { + let gz = flate2::read::GzDecoder::new(targz); + let mut archive = tar::Archive::new(gz); + let Ok(entries) = archive.entries() else { + return false; + }; + for entry in entries.flatten() { + let Ok(path) = entry.path() else { + continue; + }; + let name = path.to_string_lossy(); + if name == binary_name || name.ends_with(&format!("/{binary_name}")) { + return true; + } + } + false +} + +fn archive_contains_zip_entry(zip_bytes: &[u8], binary_name: &str) -> bool { + let cursor = std::io::Cursor::new(zip_bytes); + let Ok(mut archive) = zip::ZipArchive::new(cursor) else { + return false; + }; + for i in 0..archive.len() { + let Ok(entry) = archive.by_index(i) else { + continue; + }; + let name = entry.name(); + if name == binary_name || name.ends_with(&format!("/{binary_name}")) { + return true; + } + } + false +} + +fn hex_sha256(data: &[u8]) -> String { + sha256(data).iter().map(|b| format!("{b:02x}")).collect() +} diff --git a/rust/scripts/snapshot-bundled-in-process-version.sh b/rust/scripts/snapshot-bundled-in-process-version.sh new file mode 100755 index 0000000000..8743f9d17a --- /dev/null +++ b/rust/scripts/snapshot-bundled-in-process-version.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# +# Snapshot the Copilot CLI version + per-platform npm integrity values for the +# rust crate's bundled-in-process build path. + +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" +OUTPUT="${RUST_DIR}/cli-version-in-process.txt" + +if [[ ! -f "${LOCKFILE}" ]]; then + echo "error: ${LOCKFILE} not found" >&2 + exit 1 +fi + +VERSION="$(node -e "console.log(require('${LOCKFILE}').packages['node_modules/@github/copilot'].version)")" +if [[ -z "${VERSION}" ]]; then + echo "error: could not read @github/copilot version from ${LOCKFILE}" >&2 + exit 1 +fi + +PACKAGES=( + "copilot-darwin-arm64" + "copilot-darwin-x64" + "copilot-linux-arm64" + "copilot-linux-x64" + "copilot-linuxmusl-arm64" + "copilot-linuxmusl-x64" + "copilot-win32-arm64" + "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 + +{ + 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]}" + done +} > "${OUTPUT}" + +echo "Wrote ${OUTPUT} (version=${VERSION}, ${#PACKAGES[@]} integrity values)" diff --git a/rust/src/copilot_request_handler.rs b/rust/src/copilot_request_handler.rs index b686b6eada..961ae3876e 100644 --- a/rust/src/copilot_request_handler.rs +++ b/rust/src/copilot_request_handler.rs @@ -140,6 +140,12 @@ pub struct CopilotRequestContext { /// Id of the runtime session that triggered this request, or `None` when it /// was issued outside any session (for example the startup model catalog). pub session_id: Option, + /// Stable per-agent-instance id for the agent trajectory that issued this request. + pub agent_id: Option, + /// Id of the parent agent when this request was issued by a subagent. + pub parent_agent_id: Option, + /// Runtime classification for the interaction that produced this request. + pub interaction_type: Option, /// Transport the runtime would otherwise use. pub transport: CopilotRequestTransport, /// Absolute request URL. @@ -594,6 +600,9 @@ struct ResponseState { #[derive(Default)] struct RequestMeta { session_id: Option, + agent_id: Option, + parent_agent_id: Option, + interaction_type: Option, method: String, url: String, headers: HeaderMap, @@ -630,6 +639,9 @@ impl CopilotRequestExchange { fn set_context(&self, params: LlmInferenceHttpRequestStartRequest) { let _ = self.meta.set(RequestMeta { session_id: params.session_id.map(SessionId::into_inner), + agent_id: params.agent_id, + parent_agent_id: params.parent_agent_id, + interaction_type: params.interaction_type, method: params.method, url: params.url, headers: headers_from_wire(¶ms.headers), @@ -649,6 +661,9 @@ impl CopilotRequestExchange { CopilotRequestContext { request_id: self.request_id.clone(), session_id: meta.session_id.clone(), + agent_id: meta.agent_id.clone(), + parent_agent_id: meta.parent_agent_id.clone(), + interaction_type: meta.interaction_type.clone(), transport: meta.transport, url: meta.url.clone(), headers: meta.headers.clone(), diff --git a/rust/src/embeddedcli.rs b/rust/src/embeddedcli.rs index 56f97e0c0e..40900a4d22 100644 --- a/rust/src/embeddedcli.rs +++ b/rust/src/embeddedcli.rs @@ -2,14 +2,11 @@ //! crate (gated on the `bundled-cli` cargo feature, which is in the default //! feature set). //! -//! build.rs downloads the platform's `copilot-{platform}.{tar.gz,zip}` -//! archive from GitHub Releases, SHA-256 verifies it against the version -//! pinned in `cli-version.txt` (or `../nodejs/package-lock.json` when -//! building inside the github/copilot-sdk repo itself), and embeds the -//! **raw archive bytes** -//! into the consumer's compiled artifact via `include_bytes!()`. Extraction -//! to a real on-disk path is deferred until the first call to -//! [`path`] / [`install_at`]. +//! Normal builds embed the platform release archive from GitHub Releases. +//! Builds with `bundled-in-process` instead embed a minimal archive from the +//! platform npm package containing the CLI executable and native runtime +//! library. Extraction to a real on-disk path is deferred until the first call +//! to [`path`] / [`install_at`]. //! //! 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. @@ -31,7 +28,7 @@ // off but still needs to exercise them. #[cfg(any(has_bundled_cli, test))] use std::fs; -#[cfg(all(has_bundled_cli, not(windows)))] +#[cfg(all(has_bundled_cli, any(feature = "bundled-in-process", not(windows))))] use std::io::Read; #[cfg(any(has_bundled_cli, test))] use std::io::Write; @@ -44,8 +41,8 @@ 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 raw archive -// bytes. The CLI version is exposed crate-wide via the +// supported, build.rs generates `bundled_cli.rs` exposing the selected archive. +// 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. #[cfg(has_bundled_cli)] @@ -157,8 +154,53 @@ fn default_install_dir(version: &str) -> PathBuf { #[cfg(has_bundled_cli)] const MAX_PUBLISH_ATTEMPTS: u32 = 3; +// Natural platform shared-library name for the in-process FFI runtime. +#[cfg(all(has_bundled_cli, feature = "bundled-in-process", windows))] +const RUNTIME_LIBRARY_NAME: &str = "copilot_runtime.dll"; +#[cfg(all(has_bundled_cli, feature = "bundled-in-process", target_os = "macos"))] +const RUNTIME_LIBRARY_NAME: &str = "libcopilot_runtime.dylib"; +#[cfg(all( + has_bundled_cli, + feature = "bundled-in-process", + not(windows), + not(target_os = "macos") +))] +const RUNTIME_LIBRARY_NAME: &str = "libcopilot_runtime.so"; + #[cfg(has_bundled_cli)] fn install(install_dir: &Path, archive: &[u8]) -> Result { + let final_path = install_cli(install_dir, archive)?; + #[cfg(feature = "bundled-in-process")] + { + install_runtime_library(install_dir, archive)?; + } + Ok(final_path) +} + +#[cfg(all(has_bundled_cli, feature = "bundled-in-process"))] +fn install_runtime_library(install_dir: &Path, archive: &[u8]) -> Result<(), EmbeddedCliError> { + let target = install_dir.join(RUNTIME_LIBRARY_NAME); + if fs::metadata(&target).map(|m| m.len() > 0).unwrap_or(false) { + return Ok(()); + } + let bytes = extract_binary(archive, RUNTIME_LIBRARY_NAME)?; + if bytes.is_empty() { + return Err(EmbeddedCliError::with_message( + EmbeddedCliErrorKind::Verification, + "embedded runtime library is empty", + )); + } + let tmp = write_temp_file(install_dir, &bytes)?; + if let Err(e) = publish(&tmp, &target) { + let _ = fs::remove_file(&tmp); + return Err(e); + } + tracing::debug!(path = %target.display(), "in-process FFI runtime library installed"); + Ok(()) +} + +#[cfg(has_bundled_cli)] +fn install_cli(install_dir: &Path, archive: &[u8]) -> Result { let verbose = std::env::var("COPILOT_CLI_INSTALL_VERBOSE").ok().as_deref() == Some("1"); fs::create_dir_all(install_dir) @@ -438,7 +480,7 @@ fn read_marker_len(marker_path: &Path) -> Option { .ok() } -#[cfg(all(has_bundled_cli, not(windows)))] +#[cfg(all(has_bundled_cli, any(feature = "bundled-in-process", not(windows))))] fn extract_binary(archive: &[u8], binary_name: &str) -> Result, EmbeddedCliError> { let gz = flate2::read::GzDecoder::new(archive); let mut tar = tar::Archive::new(gz); @@ -463,7 +505,7 @@ fn extract_binary(archive: &[u8], binary_name: &str) -> Result, Embedded Err(EmbeddedCliErrorKind::BinaryNotFoundInArchive.into()) } -#[cfg(all(has_bundled_cli, windows))] +#[cfg(all(has_bundled_cli, not(feature = "bundled-in-process"), windows))] fn extract_binary(archive: &[u8], binary_name: &str) -> Result, EmbeddedCliError> { let cursor = std::io::Cursor::new(archive); let mut zip = zip::ZipArchive::new(cursor) @@ -499,9 +541,9 @@ fn sanitize_version(version: &str) -> String { #[allow(dead_code)] enum EmbeddedCliErrorKind { CreateDir, - #[cfg(not(windows))] + #[cfg(any(feature = "bundled-in-process", not(windows)))] Archive, - #[cfg(windows)] + #[cfg(all(not(feature = "bundled-in-process"), windows))] Zip, BinaryNotFoundInArchive, Io, @@ -519,9 +561,9 @@ impl std::fmt::Display for EmbeddedCliErrorKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { EmbeddedCliErrorKind::CreateDir => f.write_str("failed to create install directory"), - #[cfg(not(windows))] + #[cfg(any(feature = "bundled-in-process", not(windows)))] EmbeddedCliErrorKind::Archive => f.write_str("failed to read archive entry"), - #[cfg(windows)] + #[cfg(all(not(feature = "bundled-in-process"), windows))] EmbeddedCliErrorKind::Zip => f.write_str("failed to read zip archive"), EmbeddedCliErrorKind::BinaryNotFoundInArchive => { f.write_str("CLI binary not found in embedded archive") @@ -626,6 +668,33 @@ impl std::error::Error for EmbeddedCliError { mod tests { use super::*; + #[cfg(all(has_bundled_cli, feature = "bundled-in-process"))] + #[test] + fn embedded_archive_contains_only_expected_files() { + let gz = flate2::read::GzDecoder::new(build_time::CLI_ARCHIVE); + let mut archive = tar::Archive::new(gz); + let mut names: Vec = archive + .entries() + .expect("archive entries") + .map(|entry| { + entry + .expect("archive entry") + .path() + .expect("archive path") + .to_string_lossy() + .into_owned() + }) + .collect(); + names.sort(); + + let mut expected = vec![ + CLI_BINARY_NAME.to_string(), + RUNTIME_LIBRARY_NAME.to_string(), + ]; + expected.sort(); + assert_eq!(names, expected); + } + /// Bytes whose header looks like a valid executable image on the host /// platform, so `looks_like_valid_image` accepts them. `extra` padding /// bytes follow the magic so size checks have something to disagree about. diff --git a/rust/src/ffi.rs b/rust/src/ffi.rs new file mode 100644 index 0000000000..f784b1a6d1 --- /dev/null +++ b/rust/src/ffi.rs @@ -0,0 +1,633 @@ +//! In-process FFI transport: hosts the Copilot runtime by loading its native +//! library and speaking JSON-RPC over its C ABI, +//! instead of spawning a CLI child process and communicating over stdio/TCP. +//! +//! The runtime's `host_start` export spawns the residual TypeScript worker +//! itself — the packaged single-file CLI (`copilot --embedded-host`) or, for +//! dev, `node dist-cli/index.js --embedded-host`. JSON-RPC frames are pumped +//! across the ABI: writes go to `connection_write`; inbound frames arrive on a +//! native callback that feeds an async reader. The framing is unchanged — the +//! same LSP `Content-Length:` frames the stdio transport uses. + +use std::collections::HashMap; +use std::ffi::c_void; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, AtomicUsize, Ordering}; +use std::sync::{Arc, OnceLock}; +use std::task::{Context, Poll}; + +use libloading::Library; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio::sync::mpsc; +use tracing::debug; + +use crate::{Error, ErrorKind}; + +type OutboundCallback = unsafe extern "C" fn(*mut c_void, *const u8, usize); +type HostStartFn = unsafe extern "C" fn(*const u8, usize, *const u8, usize) -> u32; +type HostShutdownFn = unsafe extern "C" fn(u32) -> bool; +#[allow(clippy::type_complexity)] +type ConnectionOpenFn = unsafe extern "C" fn( + u32, + OutboundCallback, + *mut c_void, + *const u8, + usize, + *const u8, + usize, + *const u8, + usize, +) -> u32; +type ConnectionWriteFn = unsafe extern "C" fn(u32, *const u8, usize) -> bool; +type ConnectionCloseFn = unsafe extern "C" fn(u32) -> bool; + +/// State handed to the native side as `user_data` so the outbound callback can +/// route inbound frames back to the reader. +struct CallbackState { + tx: mpsc::UnboundedSender>, + active_callbacks: AtomicUsize, + closing: AtomicBool, +} + +extern "C" fn on_outbound(user_data: *mut c_void, bytes: *const u8, len: usize) { + if user_data.is_null() || bytes.is_null() || len == 0 { + return; + } + let state = unsafe { &*(user_data as *const CallbackState) }; + state.active_callbacks.fetch_add(1, Ordering::SeqCst); + if state.closing.load(Ordering::SeqCst) { + state.active_callbacks.fetch_sub(1, Ordering::SeqCst); + return; + } + let slice = unsafe { std::slice::from_raw_parts(bytes, len) }; + let _ = state.tx.send(slice.to_vec()); + state.active_callbacks.fetch_sub(1, Ordering::SeqCst); +} + +/// Bound exports and connection lifecycle state, shared between the +/// [`FfiWriter`] and the owning [`Client`]. The cdylib itself is loaded +/// process-globally and never unloaded (see [`load_library`]), so this holds +/// only the bound fn pointers and connection state. +pub(crate) struct FfiShared { + host_shutdown: HostShutdownFn, + connection_write: ConnectionWriteFn, + connection_close: ConnectionCloseFn, + server_id: AtomicU32, + connection_id: AtomicU32, + callback_state: AtomicPtr, + closed: AtomicBool, + operation_lock: parking_lot::Mutex<()>, + library_path: PathBuf, +} + +// The raw fn pointers and the boxed callback state are safe to move across +// threads: the native side copies buffers synchronously and the callback only +// forwards to a thread-safe channel sender. +unsafe impl Send for FfiShared {} +unsafe impl Sync for FfiShared {} + +impl FfiShared { + /// Close the connection, shut the host down, and free the callback state. + /// Idempotent; called from [`Client::stop`], drop, and on startup failure. + pub(crate) fn close(&self) { + let _operation = self.operation_lock.lock(); + if self.closed.swap(true, Ordering::SeqCst) { + return; + } + let state = self.callback_state.load(Ordering::SeqCst); + if !state.is_null() { + unsafe { &*state }.closing.store(true, Ordering::SeqCst); + } + let conn = self.connection_id.swap(0, Ordering::SeqCst); + if conn != 0 { + unsafe { (self.connection_close)(conn) }; + } + let server = self.server_id.swap(0, Ordering::SeqCst); + if server != 0 { + unsafe { (self.host_shutdown)(server) }; + } + // Free the callback state only after the connection is closed and the + // host is shut down, so native can no longer invoke the callback. + let state = self + .callback_state + .swap(std::ptr::null_mut(), Ordering::SeqCst); + if !state.is_null() { + while unsafe { &*state }.active_callbacks.load(Ordering::SeqCst) != 0 { + std::thread::yield_now(); + } + drop(unsafe { Box::from_raw(state) }); + } + debug!(library = %self.library_path.display(), "FFI runtime connection closed"); + } + + fn write_frame(&self, frame: &[u8]) -> bool { + let _operation = self.operation_lock.lock(); + if self.closed.load(Ordering::SeqCst) { + return false; + } + let conn = self.connection_id.load(Ordering::SeqCst); + if conn == 0 { + return false; + } + unsafe { (self.connection_write)(conn, frame.as_ptr(), frame.len()) } + } +} + +impl Drop for FfiShared { + fn drop(&mut self) { + self.close(); + } +} + +/// Read side of the FFI transport, fed by the native outbound callback via an +/// unbounded channel. Implements [`AsyncRead`] for the JSON-RPC read loop. +pub(crate) struct FfiReader { + rx: mpsc::UnboundedReceiver>, + leftover: Vec, + pos: usize, +} + +impl AsyncRead for FfiReader { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + if self.pos >= self.leftover.len() { + match self.rx.poll_recv(cx) { + Poll::Ready(Some(chunk)) => { + self.leftover = chunk; + self.pos = 0; + } + Poll::Ready(None) => return Poll::Ready(Ok(())), + Poll::Pending => return Poll::Pending, + } + } + let available = self.leftover.len() - self.pos; + let n = available.min(buf.remaining()); + let start = self.pos; + buf.put_slice(&self.leftover[start..start + n]); + self.pos += n; + Poll::Ready(Ok(())) + } +} + +/// Write side of the FFI transport. Each frame is forwarded synchronously to +/// the native `connection_write` export (native copies before returning). +pub(crate) struct FfiWriter { + shared: Arc, +} + +impl AsyncWrite for FfiWriter { + fn poll_write( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + if self.shared.write_frame(buf) { + Poll::Ready(Ok(buf.len())) + } else { + Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "failed to write a frame to the in-process runtime connection", + ))) + } + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +/// Prepared FFI host: the bound cdylib exports plus the spawn arguments needed +/// to start the runtime worker. The cdylib is loaded process-globally and never +/// unloaded (see [`load_library`]). +pub(crate) struct FfiHost { + library_path: PathBuf, + entrypoint: PathBuf, + environment: Vec<(String, String)>, + args: Vec, + host_start: HostStartFn, + host_shutdown: HostShutdownFn, + connection_open: ConnectionOpenFn, + connection_write: ConnectionWriteFn, + connection_close: ConnectionCloseFn, +} + +// SAFETY: as for `FfiShared` — the bound exports are plain fn pointers, safe to +// move to the blocking thread that starts the host. +unsafe impl Send for FfiHost {} + +impl FfiHost { + /// Load the cdylib next to `entrypoint` and bind its exports. + /// + /// `entrypoint` is the packaged single-file CLI binary or, for dev, a + /// `.js` file launched via `node`. The native library is resolved relative + /// to the entrypoint directory, supporting both packaged and development + /// layouts. + pub(crate) fn create( + entrypoint: &Path, + environment: Vec<(String, String)>, + args: Vec, + ) -> Result { + let entrypoint = std::fs::canonicalize(entrypoint) + .map(path_for_child_process) + .map_err(|e| { + Error::with_message( + ErrorKind::InvalidConfig, + format!( + "failed to resolve in-process CLI entrypoint '{}': {e}", + entrypoint.display() + ), + ) + })?; + let library_path = + std::fs::canonicalize(resolve_library_path(&entrypoint)?).map_err(|e| { + Error::with_message( + ErrorKind::InvalidConfig, + format!("failed to resolve in-process runtime library: {e}"), + ) + })?; + let lib = load_library(&library_path)?; + + let host_start = *bind::(lib, b"copilot_runtime_host_start\0", &library_path)?; + let host_shutdown = + *bind::(lib, b"copilot_runtime_host_shutdown\0", &library_path)?; + let connection_open = + *bind::(lib, b"copilot_runtime_connection_open\0", &library_path)?; + let connection_write = + *bind::(lib, b"copilot_runtime_connection_write\0", &library_path)?; + let connection_close = + *bind::(lib, b"copilot_runtime_connection_close\0", &library_path)?; + + Ok(Self { + library_path, + entrypoint, + environment, + args, + host_start, + host_shutdown, + connection_open, + connection_write, + connection_close, + }) + } + + /// Start the runtime worker and open the FFI JSON-RPC connection. + /// + /// `host_start` blocks until the worker connects back and signals + /// readiness (up to ~30s), and must not run on an async executor thread, so + /// the blocking handshake is offloaded to [`tokio::task::spawn_blocking`]. + pub(crate) async fn start(self) -> Result<(FfiReader, FfiWriter, Arc), Error> { + tokio::task::spawn_blocking(move || self.start_blocking()) + .await + .map_err(|e| { + Error::with_message( + ErrorKind::InvalidConfig, + format!("in-process runtime startup task failed: {e}"), + ) + })? + } + + fn start_blocking(self) -> Result<(FfiReader, FfiWriter, Arc), Error> { + let argv = build_argv_json(&self.entrypoint, &self.args); + let env = build_env_json(&self.environment); + + let (env_ptr, env_len) = match &env { + Some(bytes) => (bytes.as_ptr(), bytes.len()), + None => (std::ptr::null(), 0), + }; + + let server_id = unsafe { (self.host_start)(argv.as_ptr(), argv.len(), env_ptr, env_len) }; + + if server_id == 0 { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + format!( + "copilot_runtime_host_start failed (library '{}', entrypoint '{}')", + self.library_path.display(), + self.entrypoint.display() + ), + )); + } + + let (tx, rx) = mpsc::unbounded_channel::>(); + let state_ptr = Box::into_raw(Box::new(CallbackState { + tx, + active_callbacks: AtomicUsize::new(0), + closing: AtomicBool::new(false), + })); + let connection_id = unsafe { + (self.connection_open)( + server_id, + on_outbound, + state_ptr as *mut c_void, + std::ptr::null(), + 0, + std::ptr::null(), + 0, + std::ptr::null(), + 0, + ) + }; + if connection_id == 0 { + drop(unsafe { Box::from_raw(state_ptr) }); + unsafe { (self.host_shutdown)(server_id) }; + return Err(Error::with_message( + ErrorKind::InvalidConfig, + "copilot_runtime_connection_open failed", + )); + } + + let shared = Arc::new(FfiShared { + host_shutdown: self.host_shutdown, + connection_write: self.connection_write, + connection_close: self.connection_close, + server_id: AtomicU32::new(server_id), + connection_id: AtomicU32::new(connection_id), + callback_state: AtomicPtr::new(state_ptr), + closed: AtomicBool::new(false), + operation_lock: parking_lot::Mutex::new(()), + library_path: self.library_path.clone(), + }); + + debug!( + library = %self.library_path.display(), + server_id, connection_id, "FFI runtime host started" + ); + + let reader = FfiReader { + rx, + leftover: Vec::new(), + pos: 0, + }; + let writer = FfiWriter { + shared: Arc::clone(&shared), + }; + Ok((reader, writer, shared)) + } +} + +fn bind<'lib, T>( + lib: &'lib Library, + symbol: &[u8], + library_path: &Path, +) -> Result, Error> { + match unsafe { lib.get::(symbol) } { + Ok(export) => Ok(export), + Err(e) => Err(Error::with_message( + ErrorKind::InvalidConfig, + format!( + "in-process runtime library '{}' is missing an expected export ({}): {e}", + library_path.display(), + String::from_utf8_lossy(symbol.strip_suffix(b"\0").unwrap_or(symbol)) + ), + )), + } +} + +/// Loads the runtime cdylib once per process and never unloads it, returning a +/// `'static` reference. Subsequent loads of the same path reuse the first +/// handle. +/// +/// The library stays mapped because native worker threads can outlive an +/// individual connection teardown. +fn load_library(library_path: &Path) -> Result<&'static Library, Error> { + static LIBRARIES: OnceLock>> = + OnceLock::new(); + let cache = LIBRARIES.get_or_init(|| parking_lot::Mutex::new(HashMap::new())); + + let mut guard = cache.lock(); + if let Some(lib) = guard.get(library_path) { + return Ok(*lib); + } + + let lib = unsafe { Library::new(library_path) }.map_err(|e| { + Error::with_message( + ErrorKind::InvalidConfig, + format!( + "failed to load in-process runtime library '{}': {e}", + library_path.display() + ), + ) + })?; + // Leak the library so it is never unloaded for the process lifetime. + let leaked: &'static Library = Box::leak(Box::new(lib)); + guard.insert(library_path.to_path_buf(), leaked); + Ok(leaked) +} + +/// The natural platform shared-library file name for the runtime cdylib — the +/// `.node` file renamed to what the Rust cdylib would be called on this OS. +fn natural_library_name() -> &'static str { + if cfg!(windows) { + "copilot_runtime.dll" + } else if cfg!(target_os = "macos") { + "libcopilot_runtime.dylib" + } else { + "libcopilot_runtime.so" + } +} + +/// The package prebuild folder name for the current host. +pub(crate) fn prebuilds_folder() -> Option { + let platform = if cfg!(target_os = "windows") { + "win32" + } else if cfg!(target_os = "macos") { + "darwin" + } else if cfg!(target_os = "linux") { + "linux" + } else { + return None; + }; + let arch = if cfg!(target_arch = "x86_64") { + "x64" + } else if cfg!(target_arch = "aarch64") { + "arm64" + } else { + return None; + }; + Some(format!("{platform}-{arch}")) +} + +fn resolve_library_path(entrypoint: &Path) -> Result { + let dir = entrypoint.parent().ok_or_else(|| { + Error::with_message( + ErrorKind::InvalidConfig, + format!( + "could not determine directory for CLI entrypoint '{}'", + entrypoint.display() + ), + ) + })?; + + // Bundled/flat layout: natural shared-library name next to the CLI. + let flat = dir.join(natural_library_name()); + if flat.is_file() { + return Ok(flat); + } + + // Development package layout. + let prebuilds = + prebuilds_folder().map(|folder| dir.join("prebuilds").join(folder).join("runtime.node")); + if let Some(prebuilds_path) = &prebuilds + && prebuilds_path.is_file() + { + return Ok(prebuilds_path.clone()); + } + + Err(Error::with_message( + ErrorKind::BinaryNotFound { + name: natural_library_name().into(), + hint: Some(format!( + "native runtime library not found next to '{}'. Enable the \ + `bundled-in-process` feature or set COPILOT_CLI_PATH to a compatible CLI package.", + entrypoint.display() + )), + }, + "native runtime library not found", + )) +} + +#[cfg(windows)] +fn path_for_child_process(path: PathBuf) -> PathBuf { + use std::ffi::OsString; + use std::os::windows::ffi::{OsStrExt, OsStringExt}; + + const VERBATIM_PREFIX: &[u16] = &[b'\\' as u16, b'\\' as u16, b'?' as u16, b'\\' as u16]; + const UNC_PREFIX: &[u16] = &[b'U' as u16, b'N' as u16, b'C' as u16, b'\\' as u16]; + + let encoded: Vec = path.as_os_str().encode_wide().collect(); + let Some(stripped) = encoded.strip_prefix(VERBATIM_PREFIX) else { + return path; + }; + let normalized = if let Some(unc_path) = stripped.strip_prefix(UNC_PREFIX) { + let mut result = vec![b'\\' as u16, b'\\' as u16]; + result.extend_from_slice(unc_path); + result + } else { + stripped.to_vec() + }; + PathBuf::from(OsString::from_wide(&normalized)) +} + +#[cfg(not(windows))] +fn path_for_child_process(path: PathBuf) -> PathBuf { + path +} + +fn build_argv_json(entrypoint: &Path, extra_args: &[String]) -> Vec { + // A `.js` entrypoint (dev / dist-cli) is launched via node; the packaged + // single-file CLI binary embeds its own Node and is invoked directly. + let entrypoint_str = entrypoint.to_string_lossy().into_owned(); + let is_js = entrypoint + .extension() + .and_then(|ext| ext.to_str()) + .is_some_and(|ext| ext.eq_ignore_ascii_case("js")); + let mut argv: Vec = if is_js { + vec![ + "node".to_string(), + entrypoint_str, + "--embedded-host".to_string(), + "--no-auto-update".to_string(), + ] + } else { + vec![ + entrypoint_str, + "--embedded-host".to_string(), + "--no-auto-update".to_string(), + ] + }; + argv.extend_from_slice(extra_args); + serde_json::to_vec(&argv).expect("argv serializes") +} + +fn build_env_json(environment: &[(String, String)]) -> Option> { + if environment.is_empty() { + return None; + } + let map: serde_json::Map = environment + .iter() + .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone()))) + .collect(); + Some(serde_json::to_vec(&map).expect("env serializes")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn argv_pins_worker_and_appends_client_options() { + let argv: Vec = serde_json::from_slice(&build_argv_json( + Path::new("copilot"), + &["--log-level".into(), "debug".into()], + )) + .unwrap(); + + assert_eq!( + argv, + [ + "copilot", + "--embedded-host", + "--no-auto-update", + "--log-level", + "debug" + ] + ); + } + + #[test] + fn javascript_entrypoint_uses_node() { + let argv: Vec = + serde_json::from_slice(&build_argv_json(Path::new("index.js"), &[])).unwrap(); + + assert_eq!( + argv, + ["node", "index.js", "--embedded-host", "--no-auto-update"] + ); + } + + #[cfg(windows)] + #[test] + fn child_process_path_removes_windows_verbatim_prefix() { + assert_eq!( + path_for_child_process(PathBuf::from(r"\\?\D:\a\copilot-sdk\index.js")), + PathBuf::from(r"D:\a\copilot-sdk\index.js") + ); + assert_eq!( + path_for_child_process(PathBuf::from(r"\\?\UNC\server\share\copilot-sdk\index.js")), + PathBuf::from(r"\\server\share\copilot-sdk\index.js") + ); + } + + #[test] + fn environment_is_omitted_when_empty() { + assert_eq!(build_env_json(&[]), None); + } + + #[test] + fn environment_serializes_worker_overrides() { + let env: serde_json::Value = serde_json::from_slice( + &build_env_json(&[ + ("COPILOT_HOME".into(), "state".into()), + ("COPILOT_DISABLE_KEYTAR".into(), "1".into()), + ]) + .unwrap(), + ) + .unwrap(); + + assert_eq!( + env, + serde_json::json!({ + "COPILOT_HOME": "state", + "COPILOT_DISABLE_KEYTAR": "1", + }) + ); + } +} diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index b7441255a0..e243ec1dc2 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; use super::session_events::{ AbortReason, ContextTier, McpServerSource, McpServerStatus, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, - UserToolSessionApproval, + UserToolSessionApproval, Verbosity, }; use crate::types::{RequestId, SessionEvent, SessionId}; @@ -92,6 +92,8 @@ pub mod rpc_methods { pub const INSTRUCTIONS_DISCOVER: &str = "instructions.discover"; /// `instructions.getDiscoveryPaths` pub const INSTRUCTIONS_GETDISCOVERYPATHS: &str = "instructions.getDiscoveryPaths"; + /// `commands.list` + pub const COMMANDS_LIST: &str = "commands.list"; /// `user.settings.reload` pub const USER_SETTINGS_RELOAD: &str = "user.settings.reload"; /// `user.settings.get` @@ -171,6 +173,8 @@ pub mod rpc_methods { pub const SESSION_SUSPEND: &str = "session.suspend"; /// `session.send` pub const SESSION_SEND: &str = "session.send"; + /// `session.sendMessages` + pub const SESSION_SENDMESSAGES: &str = "session.sendMessages"; /// `session.abort` pub const SESSION_ABORT: &str = "session.abort"; /// `session.shutdown` @@ -344,6 +348,12 @@ pub mod rpc_methods { pub const SESSION_MCP_APPS_GETHOSTCONTEXT: &str = "session.mcp.apps.getHostContext"; /// `session.mcp.apps.diagnose` pub const SESSION_MCP_APPS_DIAGNOSE: &str = "session.mcp.apps.diagnose"; + /// `session.mcp.resources.read` + pub const SESSION_MCP_RESOURCES_READ: &str = "session.mcp.resources.read"; + /// `session.mcp.resources.list` + pub const SESSION_MCP_RESOURCES_LIST: &str = "session.mcp.resources.list"; + /// `session.mcp.resources.listTemplates` + pub const SESSION_MCP_RESOURCES_LISTTEMPLATES: &str = "session.mcp.resources.listTemplates"; /// `session.plugins.list` pub const SESSION_PLUGINS_LIST: &str = "session.plugins.list"; /// `session.plugins.reload` @@ -2208,6 +2218,9 @@ pub struct DiscoveredCanvas { /// Owning extension display name, when available #[serde(skip_serializing_if = "Option::is_none")] pub extension_name: Option, + /// Host-local PNG path for the canvas icon, when supplied + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, /// JSON Schema for canvas open input #[serde(skip_serializing_if = "Option::is_none")] pub input_schema: Option, @@ -2246,6 +2259,9 @@ pub struct OpenCanvasInstance { /// Owning extension display name, when available #[serde(skip_serializing_if = "Option::is_none")] pub extension_name: Option, + /// Host-local PNG path for the canvas icon, when supplied + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, /// Input supplied when the instance was opened #[serde(skip_serializing_if = "Option::is_none")] pub input: Option, @@ -4039,6 +4055,24 @@ pub struct HMACAuthInfo { pub r#type: HMACAuthInfoType, } +/// Runtime-owned wire payload for a server-to-client hook callback invocation. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct HookInvokeRequest { + #[doc(hidden)] + pub(crate) hook_type: HookType, + pub input: serde_json::Value, + pub session_id: SessionId, +} + +/// Optional output returned by an SDK callback hook. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct HookInvokeResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub output: Option, +} + /// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. /// ///
@@ -5442,7 +5476,26 @@ pub struct McpListToolsRequest { pub server_name: String, } -/// MCP tool metadata with tool name and optional description. +/// Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block. +/// +///
+/// +/// **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 McpToolUi { + /// URI of the tool's MCP App resource, typically a `ui://` resource identifier. Use `session.mcp.resources.read` to fetch its HTML and resource metadata. + #[serde(skip_serializing_if = "Option::is_none")] + pub resource_uri: Option, + /// Tool visibility advertised by the server. When absent, MCP Apps defaults apply. + #[serde(skip_serializing_if = "Option::is_none")] + pub visibility: Option>, +} + +/// MCP tool metadata with tool name, optional description, and normalized MCP Apps discovery metadata. /// ///
/// @@ -5458,6 +5511,9 @@ pub struct McpTools { pub description: Option, /// Tool name. pub name: String, + /// Normalized MCP Apps discovery metadata. An empty object indicates that a valid `_meta.ui` block was present without recognized fields. + #[serde(skip_serializing_if = "Option::is_none")] + pub ui: Option, } /// Tools exposed by the connected MCP server. Throws when the server is not connected. @@ -5634,7 +5690,7 @@ pub struct McpRemoveGitHubResult { pub removed: bool, } -/// Server name and opaque configuration for an individual MCP server restart. +/// Standard MCP resource annotations plus preserved non-standard annotation fields. /// ///
/// @@ -5644,10 +5700,272 @@ pub struct McpRemoveGitHubResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub(crate) struct McpRestartServerRequest { - /// Opaque server configuration (MCPServerConfig). Marked internal: an in-process runtime shape supplied only by in-process CLI callers. - #[doc(hidden)] - pub(crate) config: serde_json::Value, +pub struct McpResourceAnnotations { + /// Server-provided non-standard annotation fields preserved from the MCP response + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_properties: Option>, + /// Intended audience roles for this resource + #[serde(skip_serializing_if = "Option::is_none")] + pub audience: Option>, + /// Last-modified timestamp hint + #[serde(skip_serializing_if = "Option::is_none")] + pub last_modified: Option, + /// Priority hint for model/client use + #[serde(skip_serializing_if = "Option::is_none")] + pub priority: Option, +} + +/// A resource icon descriptor plus preserved non-standard icon fields. +/// +///
+/// +/// **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 McpResourceIcon { + /// Server-provided non-standard icon fields preserved from the MCP response + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_properties: Option>, + /// Icon MIME type, when known + #[serde(skip_serializing_if = "Option::is_none")] + pub mime_type: Option, + /// Icon sizes hint + #[serde(skip_serializing_if = "Option::is_none")] + pub sizes: Option, + /// Icon URI + pub src: String, + /// Theme hint for this icon + #[serde(skip_serializing_if = "Option::is_none")] + pub theme: Option, +} + +/// An MCP resource descriptor (spec `Resource`): URI, name, and optional title, description, MIME type, size, icons, annotations, and metadata. Server-provided fields outside the standard descriptor shape are exposed under `additionalProperties`. +/// +///
+/// +/// **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 McpResource { + /// Resource-level metadata + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option>, + /// Server-provided non-standard descriptor fields preserved from the MCP response + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_properties: Option>, + /// Model/client annotations associated with this resource + #[serde(skip_serializing_if = "Option::is_none")] + pub annotations: Option, + /// Optional description of what this resource represents + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Icons associated with this resource + #[serde(skip_serializing_if = "Option::is_none")] + pub icons: Option>, + /// MIME type of the resource, if known + #[serde(skip_serializing_if = "Option::is_none")] + pub mime_type: Option, + /// The programmatic name of the resource + pub name: String, + /// Resource size in bytes, when known + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, + /// Optional human-readable display title + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + /// The resource URI (e.g. ui://... or file:///...) + pub uri: String, +} + +/// MCP resource content with URI, optional MIME type, text or base64 blob, and resource 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 McpResourceContent { + /// Resource-level metadata (CSP, permissions, etc.) + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option>, + /// Base64-encoded binary content + #[serde(skip_serializing_if = "Option::is_none")] + pub blob: Option, + /// MIME type of the content + #[serde(skip_serializing_if = "Option::is_none")] + pub mime_type: Option, + /// Text content (e.g. HTML) + #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option, + /// The resource URI + pub uri: String, +} + +/// MCP server whose resources to enumerate. +/// +///
+/// +/// **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 McpResourcesListRequest { + /// Opaque MCP pagination cursor from a prior `nextCursor` value + #[serde(skip_serializing_if = "Option::is_none")] + pub cursor: Option, + /// Name of the MCP server whose resources to enumerate + pub server_name: String, +} + +/// One page of resources advertised by the named MCP server. +/// +///
+/// +/// **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 McpResourcesListResult { + /// Opaque cursor for the next page, if the server has more resources + #[serde(skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, + /// Resources advertised by the server (proxied MCP `resources/list`) + pub resources: Vec, +} + +/// MCP server whose resource templates to enumerate. +/// +///
+/// +/// **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 McpResourcesListTemplatesRequest { + /// Opaque MCP pagination cursor from a prior `nextCursor` value + #[serde(skip_serializing_if = "Option::is_none")] + pub cursor: Option, + /// Name of the MCP server whose resource templates to enumerate + pub server_name: String, +} + +/// An MCP resource template descriptor (spec `ResourceTemplate`): an RFC 6570 URI template, name, and optional title, description, MIME type, icons, annotations, and metadata. Server-provided fields outside the standard descriptor shape are exposed under `additionalProperties`. +/// +///
+/// +/// **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 McpResourceTemplate { + /// Resource-template-level metadata + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option>, + /// Server-provided non-standard descriptor fields preserved from the MCP response + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_properties: Option>, + /// Model/client annotations associated with this template + #[serde(skip_serializing_if = "Option::is_none")] + pub annotations: Option, + /// Optional description of what this template is for + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Icons associated with resources matching this template + #[serde(skip_serializing_if = "Option::is_none")] + pub icons: Option>, + /// MIME type for resources matching this template, if uniform + #[serde(skip_serializing_if = "Option::is_none")] + pub mime_type: Option, + /// The programmatic name of the resource template + pub name: String, + /// Optional human-readable display title + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + /// An RFC 6570 URI template for constructing resource URIs + pub uri_template: String, +} + +/// One page of resource templates advertised by the named MCP server. +/// +///
+/// +/// **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 McpResourcesListTemplatesResult { + /// Opaque cursor for the next page, if the server has more resource templates + #[serde(skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, + /// Resource templates advertised by the server (proxied MCP `resources/templates/list`) + pub resource_templates: Vec, +} + +/// MCP server and resource URI to fetch. +/// +///
+/// +/// **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 McpResourcesReadRequest { + /// Name of the MCP server hosting the resource + pub server_name: String, + /// Resource URI + pub uri: String, +} + +/// Resource contents returned by the MCP server. +/// +///
+/// +/// **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 McpResourcesReadResult { + /// Resource contents returned by the server + pub contents: Vec, +} + +/// Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. +/// +///
+/// +/// **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 McpRestartServerRequest { + /// Replacement MCP server configuration (stdio process or remote HTTP/SSE). Omit to restart the server with its already-registered configuration (config-free restart-by-name). + #[serde(skip_serializing_if = "Option::is_none")] + pub config: Option, /// Name of the MCP server to restart pub server_name: String, } @@ -5862,7 +6180,7 @@ pub struct McpSetEnvValueModeResult { pub mode: McpSetEnvValueModeDetails, } -/// Server name and opaque configuration for an individual MCP server start. +/// Server name and configuration for an individual MCP server start. /// ///
/// @@ -5872,10 +6190,9 @@ pub struct McpSetEnvValueModeResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub(crate) struct McpStartServerRequest { - /// Opaque server configuration (MCPServerConfig). Marked internal: an in-process runtime shape supplied only by in-process CLI callers. - #[doc(hidden)] - pub(crate) config: serde_json::Value, +pub struct McpStartServerRequest { + /// MCP server configuration (stdio process or remote HTTP/SSE) + pub config: serde_json::Value, /// Name of the MCP server to start pub server_name: String, } @@ -6203,7 +6520,7 @@ pub struct MetadataRecordContextChangeRequest { #[serde(rename_all = "camelCase")] pub struct MetadataRecordContextChangeResult {} -/// Absolute path to set as the session's new working directory. +/// Absolute path to set as the session's new working directory. For local sessions the path must be absolute and exist on disk: it is validated before any session state changes, and a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote sessions record the path as-is. /// ///
/// @@ -6218,7 +6535,7 @@ pub struct MetadataSetWorkingDirectoryRequest { pub working_directory: String, } -/// Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for `process.chdir` and any related side-effects (file index, etc.); this method only updates the session's own recorded path. +/// Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects (file index, etc.); it does NOT change the process working directory (a session's cwd is per-session, not process-global). For local sessions the runtime validates the target first (an absolute path that exists on disk) and re-bases the permission primary directory; a rejected validation fails the call before anything is mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the new directory (best-effort). Remote sessions only record the path. /// ///
/// @@ -6276,6 +6593,30 @@ pub struct MetadataSnapshotRemoteMetadata { pub task_type: Option, } +/// Active server-driven promotion for a model, including its discount and expiry. +/// +///
+/// +/// **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 ModelBillingPromo { + /// Percentage discount (0-100) applied while the promotion is active. May be fractional. + #[serde(skip_serializing_if = "Option::is_none")] + pub discount_percent: Option, + /// UTC ISO 8601 timestamp marking when the promotion ends. Always present: the API only surfaces a promo whose expiry parses and is in the future. Consumers should treat a past value as expired. + pub ends_at: String, + /// Stable identifier for the promotion campaign. + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + /// Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + /// Long context tier pricing (available for models with extended context windows) /// ///
@@ -6375,6 +6716,9 @@ pub struct ModelBilling { /// Billing cost multiplier relative to the base rate #[serde(skip_serializing_if = "Option::is_none")] pub multiplier: Option, + /// Active server-driven promotion for this model, if any. Present when the model is being promoted with a time-boxed discount. + #[serde(skip_serializing_if = "Option::is_none")] + pub promo: Option, /// Token-level pricing information for this model #[serde(skip_serializing_if = "Option::is_none")] pub token_prices: Option, @@ -6725,6 +7069,9 @@ pub struct ModelSwitchToRequest { /// Reasoning summary mode to request for supported model clients #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_summary: Option, + /// Output verbosity level to request for supported models + #[serde(skip_serializing_if = "Option::is_none")] + pub verbosity: Option, } /// The model identifier active on the session after the switch. @@ -8642,7 +8989,7 @@ pub struct PluginsInstallRequest { pub working_directory: Option, } -/// Marketplace source to register. +/// Marketplace source and optional working directory for relative-path resolution. /// ///
/// @@ -8655,6 +9002,9 @@ pub struct PluginsInstallRequest { pub struct PluginsMarketplacesAddRequest { /// Marketplace source. Accepts the same forms as the CLI: "owner/repo" or "owner/repo#ref" (GitHub), an http/https/ssh URL (optionally with #ref), a git scp-style URL (user@host:path), or a local path. The marketplace's own name (from its manifest) is used as the registration key. pub source: String, + /// Working directory used to resolve relative local paths in `source`. Defaults to the server's current working directory. + #[serde(skip_serializing_if = "Option::is_none")] + pub working_directory: Option, } /// Name of the marketplace whose plugin catalog to fetch. @@ -8723,6 +9073,9 @@ pub struct PluginsReloadRequest { /// Re-run custom-agent discovery after refreshing plugins. Defaults to true. #[serde(skip_serializing_if = "Option::is_none")] pub reload_custom_agents: Option, + /// Re-discover and relaunch subprocess extensions (including plugin-shipped extensions) after refreshing plugins. Defaults to true. Has no effect when the session has no active extension controller (e.g. extensions were not requested for the session). + #[serde(skip_serializing_if = "Option::is_none")] + pub reload_extensions: Option, /// Re-load user, plugin, and (subject to `deferRepoHooks`) repo hooks. Defaults to true. Has no effect when the host has not registered a hook reloader (e.g. remote sessions). #[serde(skip_serializing_if = "Option::is_none")] pub reload_hooks: Option, @@ -9595,7 +9948,7 @@ pub struct QueueRemoveMostRecentResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RegisterEventInterestParams { - /// The event type the consumer wants the runtime to treat as 'observed' for behavior-switching gating. Some runtime code paths inspect whether any consumer is interested in a specific event type and choose a different implementation accordingly (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates the full interactive OAuth flow to the consumer; when no interest is registered the runtime installs a browserless fallback that silently reuses cached tokens). SDK clients that long-poll events do NOT automatically appear as listeners to these gating checks — they must explicitly call `registerInterest` for each event type they want the runtime to count as having a consumer. Multiple registrations for the same event type from the same or different consumers are tracked independently and must each be released. See: `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, `session_limits_exhausted.requested`, `user_input.requested`, `elicitation.requested`, `command.queued`, `exit_plan_mode.requested`. + /// The event type the consumer wants the runtime to treat as 'observed' for behavior-switching gating. Some runtime code paths inspect whether any consumer is interested in a specific event type and choose a different implementation accordingly (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates interactive OAuth token acquisition to the consumer via `mcp.oauth_required` events; when no interest is registered the runtime still attempts non-interactive reconnect from cached or refreshable tokens, and only marks the server `needs-auth` if usable credentials are unavailable — it does not open a browser or start interactive OAuth without a consumer). SDK clients that long-poll events do NOT automatically appear as listeners to these gating checks — they must explicitly call `registerInterest` for each event type they want the runtime to count as having a consumer. Multiple registrations for the same event type from the same or different consumers are tracked independently and must each be released. See: `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, `session_limits_exhausted.requested`, `user_input.requested`, `elicitation.requested`, `command.queued`, `exit_plan_mode.requested`. pub event_type: String, } @@ -9740,6 +10093,10 @@ pub struct RemoteControlConfig { pub struct RemoteControlStatusActive { /// Session id remote control is pointed at. pub attached_session_id: String, + /// True while a read-only/session-sync export is deferred, awaiting the first `user.message` before its MC session exists. Marked internal: this field is excluded from the public SDK surface and is populated only on the CLI in-process path. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) awaiting_first_message: Option, /// MC frontend URL for this session, when known. #[serde(skip_serializing_if = "Option::is_none")] pub frontend_url: Option, @@ -10291,6 +10648,89 @@ pub struct SendAttachmentsToMessageParams { pub instance_id: Option, } +/// A single user message to append to the session as part of a `session.sendMessages` 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 SendMessageItem { + /// Optional attachments (files, directories, selections, blobs, GitHub references) to include with this message + #[serde(skip_serializing_if = "Option::is_none")] + pub attachments: Option>, + /// If false, this message will not trigger a Premium Request Unit charge. User messages default to billable. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) billable: Option, + /// If provided, this is shown in the timeline instead of `prompt` + #[serde(skip_serializing_if = "Option::is_none")] + pub display_prompt: Option, + /// The user message text + pub prompt: String, + /// If set, the request will fail if the named tool is not available when this message is among the user messages at the start of the current exchange + #[serde(skip_serializing_if = "Option::is_none")] + pub required_tool: Option, + /// Optional provenance tag copied to the resulting user.message event. Must match one of three forms: the literal `system`, `command-` for messages originating from a command (e.g. slash command, Mission Control command), or `schedule-` for messages originating from a scheduled job. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) source: Option, +} + +/// Parameters for sending zero or more user messages to the session in a single turn. Remote-backed (Mission Control) sessions do not support this method and will return an error. +/// +///
+/// +/// **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 SendMessagesRequest { + /// The UI mode the agent was in when these messages were sent. Defaults to the session's current mode. + #[serde(skip_serializing_if = "Option::is_none")] + pub agent_mode: Option, + /// The user messages to append to the conversation, in order. May be empty, in which case a single turn runs over the existing history with no new user message. + pub messages: Vec, + /// How to deliver the messages. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, + /// If true, adds the messages to the front of the queue instead of the end + #[serde(skip_serializing_if = "Option::is_none")] + pub prepend: Option, + /// Custom HTTP headers to include in outbound model requests for this turn. Merged with session-level provider headers; per-turn headers augment and overwrite session-level headers with the same key. + #[serde(skip_serializing_if = "Option::is_none")] + pub request_headers: Option>, + /// W3C Trace Context traceparent header for distributed tracing of this agent turn + #[serde(skip_serializing_if = "Option::is_none")] + pub traceparent: Option, + /// W3C Trace Context tracestate header for distributed tracing + #[serde(skip_serializing_if = "Option::is_none")] + pub tracestate: Option, + /// If true, await completion of the agentic loop for this turn before returning. Defaults to false (fire-and-forget). When true, the result still contains the same `messageIds`; the caller can rely on the agent having processed the messages before the call resolves. + #[serde(skip_serializing_if = "Option::is_none")] + pub wait: Option, +} + +/// Result of sending zero or more user messages +/// +///
+/// +/// **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 SendMessagesResult { + /// Unique identifiers assigned to the messages, one per provided message in order. Empty when no messages were provided. + pub message_ids: Vec, +} + /// Parameters for sending a user message to the session /// ///
@@ -11257,6 +11697,21 @@ pub struct SessionMetadataSnapshot { pub workspace_path: Option, } +/// Cost-category metadata for a CAPI model. +/// +///
+/// +/// **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 SessionModelPriceCategory { + pub id: String, + pub price_category: ModelPickerPriceCategory, +} + /// The list of models available to this session. /// ///
@@ -11270,6 +11725,9 @@ pub struct SessionMetadataSnapshot { pub struct SessionModelList { /// Available models, ordered with the most preferred default first. Includes both Copilot (CAPI) models and any registry BYOK models; a BYOK model appears under its provider-qualified selection id (`provider/id`). pub list: Vec, + /// Cost categories for the full CAPI catalog, including picker-disabled models that Auto may select. Metadata only; entries absent from `list` are not manually selectable. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_price_categories: Option>, /// Per-quota snapshots returned alongside the model list, keyed by quota type. #[serde(skip_serializing_if = "Option::is_none")] pub quota_snapshots: Option>, @@ -11411,6 +11869,9 @@ pub struct SessionOpenOptions { ///
#[serde(skip_serializing_if = "Option::is_none")] pub enable_citations: Option, + /// Opt-in: self-fetch and enforce enterprise managed settings at session bootstrap. + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_managed_settings: Option, /// Whether on-demand custom instruction discovery is enabled. #[serde(skip_serializing_if = "Option::is_none")] pub enable_on_demand_instruction_discovery: Option, @@ -11439,6 +11900,9 @@ pub struct SessionOpenOptions { /// Feature-flag values resolved by the host. #[serde(skip_serializing_if = "Option::is_none")] pub feature_flags: 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>, /// Installed plugins visible to the session. #[serde(skip_serializing_if = "Option::is_none")] pub installed_plugins: Option>, @@ -11513,9 +11977,6 @@ pub struct SessionOpenOptions { /// Resolved sandbox configuration. #[serde(skip_serializing_if = "Option::is_none")] pub sandbox_config: Option, - /// Opt-in: self-fetch enterprise managed settings at session bootstrap. - #[serde(skip_serializing_if = "Option::is_none")] - pub self_fetch_managed_settings: Option, /// Capabilities enabled for this session. #[serde(skip_serializing_if = "Option::is_none")] pub session_capabilities: Option>, @@ -11540,6 +12001,9 @@ pub struct SessionOpenOptions { /// Optional trajectory output file path. #[serde(skip_serializing_if = "Option::is_none")] pub trajectory_file: Option, + /// Initial output verbosity level for supported models. + #[serde(skip_serializing_if = "Option::is_none")] + pub verbosity: Option, /// Working directory to anchor the session. #[serde(skip_serializing_if = "Option::is_none")] pub working_directory: Option, @@ -11702,6 +12166,10 @@ pub struct SessionsOpenHandoff { pub kind: SessionsOpenHandoffKind, /// Remote session metadata for the session to hand off (typically obtained from `sessions.list` with `source: "remote"`). pub metadata: RemoteSessionMetadataValue, + /// In-process confirmation callback `(request) => boolean | Promise` invoked when the handoff needs the caller to confirm a non-fatal blocker (e.g. a repository mismatch between the current working directory and the remote session). Returning `true` proceeds with the handoff; returning `false` (or omitting the callback) aborts it. Marked internal because a function reference cannot cross the JSON-RPC boundary, for the same reasons as `onProgress`. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) on_confirm: Option, /// In-process progress callback `(update) => void` invoked for each handoff step. Marked internal because a function reference cannot cross the JSON-RPC boundary. The host-side `handoffSession` is already declared as `AsyncGenerator`; the schema layer flattens it because it does not yet support streaming methods. The wire-clean replacement is to expose the AsyncGenerator directly (or use vscode-jsonrpc `$/progress` notifications) once the schema/transport layer supports it. #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] @@ -12715,6 +13183,9 @@ pub struct SessionUpdateOptionsParams { /// Map of feature-flag IDs to their boolean enabled state. #[serde(skip_serializing_if = "Option::is_none")] pub feature_flags: 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. Set to null to remove the allowlist restriction. + #[serde(skip_serializing_if = "Option::is_none")] + pub included_builtin_agents: Option>, /// Full set of installed plugins for the session. Replaces the existing list; the runtime invalidates the skills cache only when the list materially changes. #[serde(skip_serializing_if = "Option::is_none")] pub installed_plugins: Option>, @@ -12790,6 +13261,9 @@ pub struct SessionUpdateOptionsParams { /// Optional path for trajectory output. #[serde(skip_serializing_if = "Option::is_none")] pub trajectory_file: Option, + /// Output verbosity level for supported models. + #[serde(skip_serializing_if = "Option::is_none")] + pub verbosity: Option, /// Absolute working-directory path for shell tools. #[serde(skip_serializing_if = "Option::is_none")] pub working_directory: Option, @@ -12806,6 +13280,9 @@ pub struct SessionUpdateOptionsParams { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionUpdateOptionsResult { + /// Number of hooks loaded from installed plugins, returned when installedPlugins is updated + #[serde(skip_serializing_if = "Option::is_none")] + pub plugin_hook_count: Option, /// Whether the operation succeeded pub success: bool, } @@ -13156,7 +13633,7 @@ pub struct SkillsLoadDiagnostics { pub warnings: Vec, } -/// Slash-command invocation result that submits an agent prompt, with display prompt, optional mode, and settings-change flag. +/// Slash-command invocation result that submits an agent prompt, with display prompt, optional mode, optional user-facing notice, and settings-change flag. /// ///
/// @@ -13174,6 +13651,9 @@ pub struct SlashCommandAgentPromptResult { /// Optional target session mode for the agent prompt #[serde(skip_serializing_if = "Option::is_none")] pub mode: Option, + /// Optional user-facing notice to show before the prompt is submitted + #[serde(skip_serializing_if = "Option::is_none")] + pub notice: Option, /// Prompt to submit to the agent pub prompt: String, /// True when the invocation mutated user runtime settings; consumers caching settings should refresh @@ -15476,6 +15956,21 @@ pub struct InstructionsGetDiscoveryPathsResult { pub paths: Vec, } +/// Slash commands available in the session, after applying any include/exclude filters. +/// +///
+/// +/// **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 CommandsListResult { + /// Commands available in this session + pub commands: Vec, +} + /// Result of opening a session. /// ///
@@ -15768,6 +16263,21 @@ pub struct SessionSendResult { pub message_id: String, } +/// Result of sending zero or more user messages +/// +///
+/// +/// **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 SessionSendMessagesResult { + /// Unique identifiers assigned to the messages, one per provided message in order. Empty when no messages were provided. + pub message_ids: Vec, +} + /// Result of aborting the current turn /// ///
@@ -15949,6 +16459,9 @@ pub struct SessionCanvasOpenResult { /// Owning extension display name, when available #[serde(skip_serializing_if = "Option::is_none")] pub extension_name: Option, + /// Host-local PNG path for the canvas icon, when supplied + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, /// Input supplied when the instance was opened #[serde(skip_serializing_if = "Option::is_none")] pub input: Option, @@ -16062,6 +16575,9 @@ pub struct SessionModelSetReasoningEffortResult { pub struct SessionModelListResult { /// Available models, ordered with the most preferred default first. Includes both Copilot (CAPI) models and any registry BYOK models; a BYOK model appears under its provider-qualified selection id (`provider/id`). pub list: Vec, + /// Cost categories for the full CAPI catalog, including picker-disabled models that Auto may select. Metadata only; entries absent from `list` are not manually selectable. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_price_categories: Option>, /// Per-quota snapshots returned alongside the model list, keyed by quota type. #[serde(skip_serializing_if = "Option::is_none")] pub quota_snapshots: Option>, @@ -17324,6 +17840,57 @@ pub struct SessionMcpAppsDiagnoseResult { pub server: McpAppsDiagnoseServer, } +/// Resource contents returned by the MCP server. +/// +///
+/// +/// **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 SessionMcpResourcesReadResult { + /// Resource contents returned by the server + pub contents: Vec, +} + +/// One page of resources advertised by the named MCP server. +/// +///
+/// +/// **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 SessionMcpResourcesListResult { + /// Opaque cursor for the next page, if the server has more resources + #[serde(skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, + /// Resources advertised by the server (proxied MCP `resources/list`) + pub resources: Vec, +} + +/// One page of resource templates advertised by the named MCP server. +/// +///
+/// +/// **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 SessionMcpResourcesListTemplatesResult { + /// Opaque cursor for the next page, if the server has more resource templates + #[serde(skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, + /// Resource templates advertised by the server (proxied MCP `resources/templates/list`) + pub resource_templates: Vec, +} + /// Identifies the target session. /// ///
@@ -17411,6 +17978,9 @@ pub struct SessionProviderAddResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionOptionsUpdateResult { + /// Number of hooks loaded from installed plugins, returned when installedPlugins is updated + #[serde(skip_serializing_if = "Option::is_none")] + pub plugin_hook_count: Option, /// Whether the operation succeeded pub success: bool, } @@ -18470,7 +19040,7 @@ pub struct SessionMetadataGetContextHeaviestMessagesResult { #[serde(rename_all = "camelCase")] pub struct SessionMetadataRecordContextChangeResult {} -/// Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for `process.chdir` and any related side-effects (file index, etc.); this method only updates the session's own recorded path. +/// Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects (file index, etc.); it does NOT change the process working directory (a session's cwd is per-session, not process-global). For local sessions the runtime validates the target first (an absolute path that exists on disk) and re-bases the permission primary directory; a rejected validation fails the call before anything is mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the new directory (best-effort). Remote sessions only record the path. /// ///
/// @@ -20278,6 +20848,63 @@ pub enum HMACAuthInfoType { Hmac, } +/// 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 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 { @@ -20702,6 +21329,28 @@ pub enum McpHeadersHandlePendingHeadersRefreshRequest { None(McpHeadersHandlePendingHeadersRefreshRequestNone), } +/// Consumer allowed to call an MCP tool. +/// +///
+/// +/// **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 McpToolUiVisibility { + /// The model may call the tool. + #[serde(rename = "model")] + Model, + /// An MCP App view may call the tool. + #[serde(rename = "app")] + App, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum McpOauthPendingRequestResponseTokenKind { #[serde(rename = "token")] diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index b11a689fcf..403933a27c 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -43,6 +43,13 @@ impl<'a> ClientRpc<'a> { } } + /// `commands.*` sub-namespace. + pub fn commands(&self) -> ClientRpcCommands<'a> { + ClientRpcCommands { + client: self.client, + } + } + /// `instructions.*` sub-namespace. pub fn instructions(&self) -> ClientRpcInstructions<'a> { ClientRpcInstructions { @@ -457,6 +464,38 @@ impl<'a> ClientRpcAgents<'a> { } } +/// `commands.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcCommands<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcCommands<'a> { + /// Lists the well-known built-in slash commands that work as the first message in a new session (e.g. /plan, /env), without requiring an active session. Commands that depend on session state, authentication, or a synced session are omitted. + /// + /// Wire method: `commands.list`. + /// + /// # Returns + /// + /// Slash commands available in the session, after applying any include/exclude filters. + /// + ///
+ /// + /// **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 list(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::COMMANDS_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `instructions.*` RPCs. #[derive(Clone, Copy)] pub struct ClientRpcInstructions<'a> { @@ -1136,7 +1175,7 @@ impl<'a> ClientRpcPluginsMarketplaces<'a> { /// /// # Parameters /// - /// * `params` - Marketplace source to register. + /// * `params` - Marketplace source and optional working directory for relative-path resolution. /// /// # Returns /// @@ -2848,6 +2887,39 @@ impl<'a> SessionRpc<'a> { Ok(serde_json::from_value(_value)?) } + /// Sends zero or more user messages to the session in a single turn and returns their message IDs. All provided messages are appended to the conversation in order, then exactly one agent turn runs over the resulting history. When the list is empty, one turn runs over the existing history with no new user message. Remote-backed (Mission Control) sessions do not support this method and will return an error. + /// + /// Wire method: `session.sendMessages`. + /// + /// # Parameters + /// + /// * `params` - Parameters for sending zero or more user messages to the session in a single turn. Remote-backed (Mission Control) sessions do not support this method and will return an error. + /// + /// # Returns + /// + /// Result of sending zero or more user messages + /// + ///
+ /// + /// **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 send_messages( + &self, + params: SendMessagesRequest, + ) -> 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_SENDMESSAGES, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Aborts the current agent turn. /// /// Wire method: `session.abort`. @@ -4245,6 +4317,13 @@ impl<'a> SessionRpcMcp<'a> { } } + /// `session.mcp.resources.*` sub-namespace. + pub fn resources(&self) -> SessionRpcMcpResources<'a> { + SessionRpcMcpResources { + session: self.session, + } + } + /// Lists MCP servers configured for the session, their connection status, and host-level state. The host-level state (disabled/filtered servers, failed/needs-auth/pending connections, mcp3p policy, full config) is empty/zero when no MCP host has been initialized for the session. /// /// Wire method: `session.mcp.list`. @@ -4270,7 +4349,7 @@ impl<'a> SessionRpcMcp<'a> { Ok(serde_json::from_value(_value)?) } - /// Lists the tools exposed by a connected MCP server on this session's host. + /// Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session. /// /// Wire method: `session.mcp.listTools`. /// @@ -4569,13 +4648,13 @@ impl<'a> SessionRpcMcp<'a> { Ok(serde_json::from_value(_value)?) } - /// Starts an individual MCP server on the session's host. + /// Starts an individual MCP server on the live session from a caller-supplied config. Session-scoped and ephemeral: the server is added to this session's running set only and is reaped when the session ends. Does NOT modify persistent user configuration (`mcp.config.*`), so it does not affect future sessions. The server surfaces through `session.mcp.list` and the `session.mcp_servers_loaded` / `session.mcp_server_status_changed` events like any other server. /// /// Wire method: `session.mcp.startServer`. /// /// # Parameters /// - /// * `params` - Server name and opaque configuration for an individual MCP server start. + /// * `params` - Server name and configuration for an individual MCP server start. /// ///
/// @@ -4584,7 +4663,7 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn start_server(&self, params: McpStartServerRequest) -> Result<(), Error> { + pub async fn start_server(&self, params: McpStartServerRequest) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self @@ -4595,13 +4674,13 @@ impl<'a> SessionRpcMcp<'a> { Ok(()) } - /// Restarts an individual MCP server on the session's host (stops then starts). + /// Restarts an individual MCP server on the live session (stops then starts). Omit `config` for a config-free restart-by-name of an already-configured server; supply `config` to restart with a replacement configuration. Session-scoped and ephemeral: does NOT modify persistent user configuration (`mcp.config.*`). /// /// Wire method: `session.mcp.restartServer`. /// /// # Parameters /// - /// * `params` - Server name and opaque configuration for an individual MCP server restart. + /// * `params` - Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. /// ///
/// @@ -4610,10 +4689,7 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn restart_server( - &self, - params: McpRestartServerRequest, - ) -> Result<(), Error> { + pub async fn restart_server(&self, params: McpRestartServerRequest) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self @@ -5069,6 +5145,116 @@ impl<'a> SessionRpcMcpOauth<'a> { } } +/// `session.mcp.resources.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcMcpResources<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcMcpResources<'a> { + /// Fetch an MCP resource from a connected server by URI (proxies MCP `resources/read`). + /// + /// Wire method: `session.mcp.resources.read`. + /// + /// # Parameters + /// + /// * `params` - MCP server and resource URI to fetch. + /// + /// # Returns + /// + /// Resource contents returned by the MCP server. + /// + ///
+ /// + /// **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( + &self, + params: McpResourcesReadRequest, + ) -> 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_MCP_RESOURCES_READ, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Enumerate one page of resources a connected MCP server exposes (proxies MCP `resources/list`). Pass `cursor` to continue from a prior result's `nextCursor`. + /// + /// Wire method: `session.mcp.resources.list`. + /// + /// # Parameters + /// + /// * `params` - MCP server whose resources to enumerate. + /// + /// # Returns + /// + /// One page of resources advertised by the named MCP server. + /// + ///
+ /// + /// **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 list( + &self, + params: McpResourcesListRequest, + ) -> 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_MCP_RESOURCES_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Enumerate one page of resource templates a connected MCP server exposes (proxies MCP `resources/templates/list`). Pass `cursor` to continue from a prior result's `nextCursor`. + /// + /// Wire method: `session.mcp.resources.listTemplates`. + /// + /// # Parameters + /// + /// * `params` - MCP server whose resource templates to enumerate. + /// + /// # Returns + /// + /// One page of resource templates advertised by the named MCP server. + /// + ///
+ /// + /// **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 list_templates( + &self, + params: McpResourcesListTemplatesRequest, + ) -> 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_MCP_RESOURCES_LISTTEMPLATES, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `session.metadata.*` RPCs. #[derive(Clone, Copy)] pub struct SessionRpcMetadata<'a> { @@ -5287,17 +5473,17 @@ impl<'a> SessionRpcMetadata<'a> { Ok(serde_json::from_value(_value)?) } - /// Updates the session's recorded working directory. + /// Updates the session's working directory. For local sessions the target is validated first (an absolute path that exists on disk) and the permission primary directory is re-based; a rejected validation fails the call before any session state changes. /// /// Wire method: `session.metadata.setWorkingDirectory`. /// /// # Parameters /// - /// * `params` - Absolute path to set as the session's new working directory. + /// * `params` - Absolute path to set as the session's new working directory. For local sessions the path must be absolute and exist on disk: it is validated before any session state changes, and a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote sessions record the path as-is. /// /// # Returns /// - /// Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for `process.chdir` and any related side-effects (file index, etc.); this method only updates the session's own recorded path. + /// Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects (file index, etc.); it does NOT change the process working directory (a session's cwd is per-session, not process-global). For local sessions the runtime validates the target first (an absolute path that exists on disk) and re-bases the permission primary directory; a rejected validation fails the call before anything is mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the new directory (best-effort). Remote sessions only record the path. /// ///
/// diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 6505cf4765..cf670c4856 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -77,10 +77,14 @@ pub enum SessionEventType { AssistantTurnStart, #[serde(rename = "assistant.intent")] AssistantIntent, + #[serde(rename = "assistant.server_tool_progress")] + AssistantServerToolProgress, #[serde(rename = "assistant.reasoning")] AssistantReasoning, #[serde(rename = "assistant.reasoning_delta")] AssistantReasoningDelta, + #[serde(rename = "assistant.tool_call_delta")] + AssistantToolCallDelta, #[serde(rename = "assistant.streaming_delta")] AssistantStreamingDelta, #[serde(rename = "assistant.message")] @@ -184,6 +188,24 @@ pub enum SessionEventType { SessionLimitsExhaustedRequested, #[serde(rename = "session_limits_exhausted.completed")] SessionLimitsExhaustedCompleted, + /// + ///
+ /// + /// **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.auto_mode_resolved")] + SessionAutoModeResolved, + /// + ///
+ /// + /// **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.managed_settings_resolved")] + SessionManagedSettingsResolved, #[serde(rename = "commands.changed")] CommandsChanged, #[serde(rename = "capabilities.changed")] @@ -204,6 +226,12 @@ pub enum SessionEventType { SessionMcpServersLoaded, #[serde(rename = "session.mcp_server_status_changed")] SessionMcpServerStatusChanged, + #[serde(rename = "mcp.tools.list_changed")] + McpToolsListChanged, + #[serde(rename = "mcp.resources.list_changed")] + McpResourcesListChanged, + #[serde(rename = "mcp.prompts.list_changed")] + McpPromptsListChanged, #[serde(rename = "session.extensions_loaded")] SessionExtensionsLoaded, /// @@ -342,10 +370,14 @@ pub enum SessionEventData { AssistantTurnStart(AssistantTurnStartData), #[serde(rename = "assistant.intent")] AssistantIntent(AssistantIntentData), + #[serde(rename = "assistant.server_tool_progress")] + AssistantServerToolProgress(AssistantServerToolProgressData), #[serde(rename = "assistant.reasoning")] AssistantReasoning(AssistantReasoningData), #[serde(rename = "assistant.reasoning_delta")] AssistantReasoningDelta(AssistantReasoningDeltaData), + #[serde(rename = "assistant.tool_call_delta")] + AssistantToolCallDelta(AssistantToolCallDeltaData), #[serde(rename = "assistant.streaming_delta")] AssistantStreamingDelta(AssistantStreamingDeltaData), #[serde(rename = "assistant.message")] @@ -442,6 +474,24 @@ pub enum SessionEventData { SessionLimitsExhaustedRequested(SessionLimitsExhaustedRequestedData), #[serde(rename = "session_limits_exhausted.completed")] SessionLimitsExhaustedCompleted(SessionLimitsExhaustedCompletedData), + /// + ///
+ /// + /// **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.auto_mode_resolved")] + SessionAutoModeResolved(SessionAutoModeResolvedData), + /// + ///
+ /// + /// **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.managed_settings_resolved")] + SessionManagedSettingsResolved(SessionManagedSettingsResolvedData), #[serde(rename = "commands.changed")] CommandsChanged(CommandsChangedData), #[serde(rename = "capabilities.changed")] @@ -462,6 +512,12 @@ pub enum SessionEventData { SessionMcpServersLoaded(SessionMcpServersLoadedData), #[serde(rename = "session.mcp_server_status_changed")] SessionMcpServerStatusChanged(SessionMcpServerStatusChangedData), + #[serde(rename = "mcp.tools.list_changed")] + McpToolsListChanged(McpToolsListChangedData), + #[serde(rename = "mcp.resources.list_changed")] + McpResourcesListChanged(McpResourcesListChangedData), + #[serde(rename = "mcp.prompts.list_changed")] + McpPromptsListChanged(McpPromptsListChangedData), #[serde(rename = "session.extensions_loaded")] SessionExtensionsLoaded(SessionExtensionsLoadedData), /// @@ -628,6 +684,9 @@ pub struct SessionStartData { pub session_limits: Option, /// ISO 8601 timestamp when the session was created pub start_time: String, + /// Output verbosity level used for model calls, if applicable (e.g. "low", "medium", "high") + #[serde(skip_serializing_if = "Option::is_none")] + pub verbosity: Option, /// Schema version number for the session event format pub version: i64, } @@ -673,6 +732,9 @@ pub struct SessionResumeData { /// True when this resume attached to a session that the runtime already had running in-memory (for example, an extension joining a session another client was actively driving). False (or omitted) for cold resumes — the runtime had to reconstitute the session from its persisted event log. #[serde(skip_serializing_if = "Option::is_none")] pub session_was_active: Option, + /// Output verbosity level used for model calls, if applicable (e.g. "low", "medium", "high") + #[serde(skip_serializing_if = "Option::is_none")] + pub verbosity: Option, } /// Session event "session.remote_steerable_changed". Notifies that the session's remote steering capability has changed @@ -844,12 +906,18 @@ pub struct SessionModelChangeData { /// Reasoning summary mode before the model change, if applicable #[serde(skip_serializing_if = "Option::is_none")] pub previous_reasoning_summary: Option, + /// Output verbosity level before the model change, if applicable + #[serde(skip_serializing_if = "Option::is_none")] + pub previous_verbosity: Option, /// Reasoning effort level after the model change, if applicable #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, /// Reasoning summary mode after the model change, if applicable #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_summary: Option, + /// Output verbosity level after the model change, if applicable + #[serde(skip_serializing_if = "Option::is_none")] + pub verbosity: Option, } /// Session event "session.mode_changed". Agent mode change details including previous and new modes @@ -1415,6 +1483,18 @@ pub struct AssistantIntentData { pub intent: String, } +/// Session event "assistant.server_tool_progress". Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AssistantServerToolProgressData { + /// Kind of hosted server tool that is running. Only `web_search` is emitted today. + pub kind: String, + /// Position of the hosted tool call in the response output. Stable across the call's lifecycle events (unlike the provider's per-event item id, which CAPI rotates), so the host keys the live in-progress row on it. + pub output_index: i64, + /// Lifecycle status of the hosted call: `in_progress`, `searching`, or `completed`. + pub status: String, +} + /// Session event "assistant.reasoning". Assistant reasoning content for timeline display with complete thinking text #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -1435,6 +1515,22 @@ pub struct AssistantReasoningDeltaData { pub reasoning_id: String, } +/// Session event "assistant.tool_call_delta". Streaming tool-call input delta for incremental tool-call updates +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AssistantToolCallDeltaData { + /// Raw provider tool input fragment to append for this tool call. Function/tool-use providers stream serialized JSON argument text (so newlines inside JSON string values may appear as escaped `\n` until the accumulated JSON is parsed); custom tool calls stream raw custom input. + pub input_delta: String, + /// Tool call ID this delta belongs to, matching the corresponding assistant.message tool request + pub tool_call_id: String, + /// Name of the tool being invoked, when known from the stream + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_name: Option, + /// Tool call type, when known from the stream + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_type: Option, +} + /// Session event "assistant.streaming_delta". Streaming response progress with cumulative byte count #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -1596,6 +1692,9 @@ pub struct AssistantMessageData { ///
#[serde(skip_serializing_if = "Option::is_none")] pub citations: Option, + /// Client-minted request id (x-request-id header) echoed by the server. Distinct from requestId (x-github-request-id) and serviceRequestId (x-copilot-service-request-id). + #[serde(skip_serializing_if = "Option::is_none")] + pub client_request_id: Option, /// The assistant's text response content pub content: String, /// Encrypted reasoning content from OpenAI models. Session-bound and stripped on resume. @@ -2351,6 +2450,16 @@ pub struct ToolExecutionCompleteResult { /// Full detailed tool result for UI/timeline display, preserving complete content such as diffs. Falls back to content when absent. #[serde(skip_serializing_if = "Option::is_none")] pub detailed_content: Option, + /// FIDES IFC label projected from tool ingress metadata (MCP `CallToolResult._meta` or synthesized built-in ingress labels) — persisted as `{ ifc: ... }` (only the `ifc` key, not the whole `_meta`). Persisted so the FIDES IFC label survives session resume: the engine rehydrates accumulated taint by replaying these on load. Populated for ingress sources when FIDES IFC is on. Experimental. + /// + ///
+ /// + /// **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 mcp_meta: Option, /// Structured content (arbitrary JSON) returned verbatim by the MCP tool #[serde(skip_serializing_if = "Option::is_none")] pub structured_content: Option, @@ -2407,6 +2516,16 @@ pub struct ToolExecutionCompleteData { /// Whether this tool call was explicitly requested by the user rather than the assistant #[serde(skip_serializing_if = "Option::is_none")] pub is_user_requested: Option, + /// FIDES IFC label projected from tool ingress metadata (MCP `CallToolResult._meta` or synthesized built-in ingress labels). Persisted as `{ ifc: ... }` so the label survives session resume, including model-visible failure results. Experimental. + /// + ///
+ /// + /// **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 mcp_meta: Option, /// Model identifier that generated this tool call #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, @@ -2738,6 +2857,12 @@ pub struct PermissionRequestWrite { /// Complete new file contents for newly created files #[serde(skip_serializing_if = "Option::is_none")] pub new_file_contents: Option, + /// True when a built-in file tool (apply_patch / str_replace_editor) asked to write a path the sandbox filesystem policy would block, and the host opted in via sandbox.allowBypass. This is a request, not a grant: the write happens 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, + /// Justification for the sandbox-bypass request. 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 #[serde(skip_serializing_if = "Option::is_none")] pub tool_call_id: Option, @@ -2794,6 +2919,12 @@ pub struct PermissionRequestUrl { pub intention: String, /// Permission kind discriminator pub kind: PermissionRequestUrlKind, + /// 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. + #[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. + #[serde(skip_serializing_if = "Option::is_none")] + pub request_sandbox_bypass_reason: Option, /// Tool call ID that triggered this permission request #[serde(skip_serializing_if = "Option::is_none")] pub tool_call_id: Option, @@ -3052,6 +3183,12 @@ pub struct PermissionPromptRequestUrl { pub intention: String, /// Prompt kind discriminator pub kind: PermissionPromptRequestUrlKind, + /// 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. + #[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. + #[serde(skip_serializing_if = "Option::is_none")] + pub request_sandbox_bypass_reason: Option, /// Tool call ID that triggered this permission request #[serde(skip_serializing_if = "Option::is_none")] pub tool_call_id: Option, @@ -3550,6 +3687,29 @@ pub struct SamplingCompletedData { pub request_id: RequestId, } +/// Single HTTP header entry as a name/value pair. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HeaderEntry { + /// HTTP response header name as observed by the runtime. + pub name: String, + /// HTTP response header value as observed by the runtime. + pub value: String, +} + +/// Raw HTTP response details from the OAuth auth challenge, as observed by the runtime. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpOauthHttpResponse { + /// Complete UTF-8 response body for host-specific challenge handling, including an empty string for an empty body. Omitted when the complete body is not valid UTF-8; body read failures fail the HTTP operation rather than exposing a partial response. + #[serde(skip_serializing_if = "Option::is_none")] + pub body: Option, + /// HTTP response headers as observed by the runtime. Order and casing are transport-dependent, and duplicate header names may appear multiple times. + pub headers: Vec, + /// HTTP status code returned with the auth challenge. + pub status_code: i32, +} + /// Static OAuth client configuration, if the server specifies one #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -3586,6 +3746,9 @@ pub struct McpOauthWWWAuthenticateParams { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct McpOauthRequiredData { + /// Raw HTTP response details from the OAuth auth challenge, as observed by the runtime. Header order and casing are transport-dependent, and duplicate header names may appear multiple times. + #[serde(skip_serializing_if = "Option::is_none")] + pub http_response: Option, /// Why the runtime is requesting host-provided OAuth credentials. pub reason: McpOauthRequestReason, /// Unique identifier for this OAuth request; used to respond via session.mcp.oauth.handlePendingRequest @@ -3783,6 +3946,64 @@ pub struct SessionLimitsExhaustedCompletedData { pub response: SessionLimitsExhaustedResponse, } +/// Session event "session.auto_mode_resolved". Auto Intent resolution: the concrete model the session settled on for the first prompt of an auto-mode session, and why. Lets SDK clients render the chosen model and the full reason it was picked. The core selection fields (chosenModel/reasoningBucket/categoryScores) are stable; the routing-analytics fields (predictedLabel/confidence/candidateModels) mirror the upstream intent service and may evolve, hence the event's experimental stability. +/// +///
+/// +/// **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 SessionAutoModeResolvedData { + /// Ordered candidate model list the router returned, when not a fallback + #[serde(skip_serializing_if = "Option::is_none")] + pub candidate_models: Option>, + /// Per-category classifier scores (0-1) behind the bucket: the granular HYDRA capability scores (reasoning, code_gen, debugging, tool_use), or the binary needs_reasoning/no_reasoning scores when HYDRA didn't run. Lets clients show a breakdown rather than just the bucket. + #[serde(skip_serializing_if = "Option::is_none")] + pub category_scores: Option>, + /// The concrete model the session will use after any intent refinement + pub chosen_model: String, + /// Classifier confidence for the predicted label, when available + #[serde(skip_serializing_if = "Option::is_none")] + pub confidence: Option, + /// The predicted classifier label (e.g. `needs_reasoning`), when available + #[serde(skip_serializing_if = "Option::is_none")] + pub predicted_label: Option, + /// Coarse request-difficulty bucket, for explaining why a model was chosen ("picked X because this looks like high-reasoning work") + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_bucket: Option, +} + +/// Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and where they came from, so SDK clients can show users what is enterprise-managed and by which authority. 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; for a session-independent pull, use the SDK `getManagedSettings()` API, which returns the identical payload. Managed settings have a single authoritative source, so the highest-authority present layer (server > device) wins wholesale; `bypassPermissionsDisabled` is deny-wins across layers. Marked experimental while the managed-settings surface stabilizes. +/// +///
+/// +/// **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 SessionManagedSettingsResolvedData { + /// Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true. + pub bypass_permissions_disabled: bool, + /// Whether the device (MDM/plist/registry/file) managed-settings layer was present + pub device_managed: bool, + /// 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. + pub fail_closed: bool, + /// The setting keys under enterprise management in the effective managed settings (e.g. `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force. + pub managed_keys: Vec, + /// Whether the server (account/org) managed-settings layer was present + pub server_managed: bool, + /// 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, + /// Which channel supplied the effective managed settings (the winning layer), or `none` when no policy is in force + pub source: ManagedSettingsResolvedSource, +} + /// A single slash command available in the session, as listed by the `commands.changed` event. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -3986,6 +4207,30 @@ pub struct SessionMcpServerStatusChangedData { pub status: McpServerStatus, } +/// 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")] +pub struct McpToolsListChangedData { + /// Name of the MCP server whose list changed + pub server_name: String, +} + +/// Session event "mcp.resources.list_changed". Payload identifying the MCP server associated with a list change. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpResourcesListChangedData { + /// Name of the MCP server whose list changed + pub server_name: String, +} + +/// Session event "mcp.prompts.list_changed". Payload identifying the MCP server associated with a list change. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpPromptsListChangedData { + /// Name of the MCP server whose list changed + pub server_name: String, +} + /// A single extension discovered by `session.extensions_loaded`, including qualified ID, source, and current status. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -4008,7 +4253,7 @@ pub struct SessionExtensionsLoadedData { pub extensions: Vec, } -/// Session event "session.canvas.opened". Payload of `session.canvas.opened` with canvas instance and provider IDs plus optional title, status, URL, and input. +/// Session event "session.canvas.opened". Payload of `session.canvas.opened` with canvas instance and provider IDs plus optional icon, title, status, URL, and input. /// ///
/// @@ -4026,6 +4271,9 @@ pub struct SessionCanvasOpenedData { /// Owning extension display name, when available #[serde(skip_serializing_if = "Option::is_none")] pub extension_name: Option, + /// Host-local PNG path for the canvas icon, when supplied + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, /// Input supplied when the instance was opened #[serde(skip_serializing_if = "Option::is_none")] pub input: Option, @@ -4088,6 +4336,9 @@ pub struct CanvasRegistryChangedCanvas { /// Owning extension display name, when available #[serde(skip_serializing_if = "Option::is_none")] pub extension_name: Option, + /// Host-local PNG path for the canvas icon, when supplied + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, /// JSON Schema for canvas open input #[serde(skip_serializing_if = "Option::is_none")] pub input_schema: Option, @@ -4301,6 +4552,24 @@ pub enum ReasoningSummary { Unknown, } +/// Output verbosity level used for supported model calls (e.g. "low", "medium", "high") +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum Verbosity { + /// A terse response was requested. + #[serde(rename = "low")] + Low, + /// A medium amount of response detail was requested. + #[serde(rename = "medium")] + Medium, + /// A more detailed response was requested. + #[serde(rename = "high")] + High, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// The type of operation performed on the autopilot objective state file #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum AutopilotObjectiveChangedOperation { @@ -4485,6 +4754,21 @@ pub enum UserMessageDelivery { Unknown, } +/// Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AssistantMessageToolRequestType { + /// Standard function-style tool call. + #[serde(rename = "function")] + Function, + /// Custom grammar-based tool call. + #[serde(rename = "custom")] + Custom, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// The system that produced a citation. /// ///
@@ -4510,21 +4794,6 @@ pub enum CitationProvider { Unknown, } -/// Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum AssistantMessageToolRequestType { - /// Standard function-style tool call. - #[serde(rename = "function")] - Function, - /// Custom grammar-based tool call. - #[serde(rename = "custom")] - Custom, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// API endpoint used for this model call, matching CAPI supported_endpoints vocabulary #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum AssistantUsageApiEndpoint { @@ -5371,6 +5640,42 @@ pub enum SessionLimitsExhaustedResponseAction { Unknown, } +/// Coarse request-difficulty bucket for UX explainability +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AutoModeResolvedReasoningBucket { + /// The request looks low-reasoning; a lighter model is appropriate. + #[serde(rename = "low")] + Low, + /// The request needs a moderate amount of reasoning. + #[serde(rename = "medium")] + Medium, + /// The request looks high-reasoning; a stronger model is appropriate. + #[serde(rename = "high")] + High, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Which channel supplied the effective enterprise managed settings (highest-authority present layer wins wholesale) +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ManagedSettingsResolvedSource { + /// Account/org policy self-fetched from the GitHub managed-settings endpoint (higher authority). + #[serde(rename = "server")] + Server, + /// Device-level MDM policy discovered from plist/registry/file (lower authority). + #[serde(rename = "device")] + Device, + /// No managed policy is in force (no layer contributed). + #[serde(rename = "none")] + None, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Exit plan mode action #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ExitPlanModeAction { diff --git a/rust/src/hooks.rs b/rust/src/hooks.rs index ec8cdfa3a3..0c3d64076f 100644 --- a/rust/src/hooks.rs +++ b/rust/src/hooks.rs @@ -27,8 +27,8 @@ pub struct HookContext { pub struct PreToolUseInput { /// The runtime session ID of the session that triggered the hook. pub session_id: String, - /// Unix timestamp (ms). - pub timestamp: i64, + /// Unix timestamp in ms (the runtime serializes this as a JSON float). + pub timestamp: f64, /// Working directory. #[serde(rename = "cwd")] pub working_directory: PathBuf, @@ -65,8 +65,8 @@ pub struct PreToolUseOutput { pub struct PreMcpToolCallInput { /// The runtime session ID of the session that triggered the hook. pub session_id: String, - /// Unix timestamp (ms). - pub timestamp: i64, + /// Unix timestamp in ms (the runtime serializes this as a JSON float). + pub timestamp: f64, /// Working directory. #[serde(rename = "cwd")] pub working_directory: PathBuf, @@ -104,8 +104,8 @@ pub struct PreMcpToolCallOutput { pub struct PostToolUseInput { /// The runtime session ID of the session that triggered the hook. pub session_id: String, - /// Unix timestamp (ms). - pub timestamp: i64, + /// Unix timestamp in ms (the runtime serializes this as a JSON float). + pub timestamp: f64, /// Working directory. #[serde(rename = "cwd")] pub working_directory: PathBuf, @@ -144,8 +144,8 @@ pub struct PostToolUseOutput { pub struct PostToolUseFailureInput { /// The runtime session ID of the session that triggered the hook. pub session_id: String, - /// Unix timestamp (ms). - pub timestamp: i64, + /// Unix timestamp in ms (the runtime serializes this as a JSON float). + pub timestamp: f64, /// Working directory. #[serde(rename = "cwd")] pub working_directory: PathBuf, @@ -175,8 +175,8 @@ pub struct PostToolUseFailureOutput { pub struct UserPromptSubmittedInput { /// The runtime session ID of the session that triggered the hook. pub session_id: String, - /// Unix timestamp (ms). - pub timestamp: i64, + /// Unix timestamp in ms (the runtime serializes this as a JSON float). + pub timestamp: f64, /// Working directory. #[serde(rename = "cwd")] pub working_directory: PathBuf, @@ -205,8 +205,8 @@ pub struct UserPromptSubmittedOutput { pub struct SessionStartInput { /// The runtime session ID of the session that triggered the hook. pub session_id: String, - /// Unix timestamp (ms). - pub timestamp: i64, + /// Unix timestamp in ms (the runtime serializes this as a JSON float). + pub timestamp: f64, /// Working directory. #[serde(rename = "cwd")] pub working_directory: PathBuf, @@ -235,8 +235,8 @@ pub struct SessionStartOutput { pub struct SessionEndInput { /// The runtime session ID of the session that triggered the hook. pub session_id: String, - /// Unix timestamp (ms). - pub timestamp: i64, + /// Unix timestamp in ms (the runtime serializes this as a JSON float). + pub timestamp: f64, /// Working directory. #[serde(rename = "cwd")] pub working_directory: PathBuf, @@ -271,8 +271,8 @@ pub struct SessionEndOutput { pub struct ErrorOccurredInput { /// The runtime session ID of the session that triggered the hook. pub session_id: String, - /// Unix timestamp (ms). - pub timestamp: i64, + /// Unix timestamp in ms (the runtime serializes this as a JSON float). + pub timestamp: f64, /// Working directory. #[serde(rename = "cwd")] pub working_directory: PathBuf, diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 0333281f59..65d9dab215 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -10,6 +10,9 @@ mod canvas_dispatch; #[cfg(feature = "bundled-cli")] pub(crate) mod embeddedcli; mod errors; +/// In-process FFI transport hosting the runtime cdylib (`Transport::InProcess`). +#[cfg(feature = "bundled-in-process")] +pub(crate) mod ffi; pub use errors::*; /// Connection-level Copilot request handler — intercept and replace the /// model-layer HTTP and WebSocket traffic the runtime issues for both CAPI and @@ -73,7 +76,11 @@ use std::sync::{Arc, OnceLock}; use std::time::{Duration, Instant}; use async_trait::async_trait; -// JSON-RPC wire types are internal transport details (like Go SDK's internal/jsonrpc2/). +/// Re-export of [`indexmap::IndexMap`], used for order-preserving maps in the +/// public API (e.g. [`Tool::parameters`](types::Tool::parameters) and +/// `SessionConfig::mcp_servers`) so serialized key order stays deterministic. +pub use indexmap::IndexMap; +// JSON-RPC wire types are internal transport details. // External callers interact via Client/Session methods, not raw RPC. pub(crate) use jsonrpc::{ JsonRpcClient, JsonRpcError, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, error_codes, @@ -109,9 +116,25 @@ const RUNTIME_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10); #[derive(Debug, Default)] #[non_exhaustive] pub enum Transport { - /// Communicate over stdin/stdout pipes (default). + /// Resolve the transport from `COPILOT_SDK_DEFAULT_CONNECTION`, falling + /// back to [`Transport::Stdio`] when the variable is unset. #[default] + Default, + /// Communicate over stdin/stdout pipes (default). Stdio, + /// Host the runtime in-process over FFI (no child process). + /// + /// Loads the native runtime library and speaks JSON-RPC over its C ABI. + /// This is **experimental**. Per-client [`ClientOptions::program`], + /// [`ClientOptions::extra_args`], [`ClientOptions::working_directory`], + /// [`ClientOptions::env`]/[`ClientOptions::env_remove`], + /// and [`ClientOptions::telemetry`] are not supported because native + /// runtime code shares the host process. Typed runtime options such as + /// authentication, log level, and [`ClientOptions::base_directory`] remain + /// supported. + /// + /// Requires the `bundled-in-process` Cargo feature. + InProcess, /// Spawn the CLI with `--port` and connect via TCP. Tcp { /// Port to listen on (0 for OS-assigned). @@ -203,17 +226,19 @@ pub fn install_bundled_cli() -> Option { /// This skips auto-resolution entirely. #[non_exhaustive] pub struct ClientOptions { - /// How to locate the CLI binary. + /// How to locate the child-process runtime. pub program: CliProgram, /// Arguments prepended before `--server` (e.g. the script path for node). pub prefix_args: Vec, /// Working directory for the CLI process. + /// + /// Setting this option is not supported with [`Transport::InProcess`]. pub working_directory: PathBuf, /// Environment variables set on the child process. pub env: Vec<(OsString, OsString)>, /// Environment variable names to remove from the child process. pub env_remove: Vec, - /// Extra CLI flags appended after the transport-specific arguments. + /// Extra flags for child-process transports. pub extra_args: Vec, /// Transport mode used to communicate with the CLI server. pub transport: Transport, @@ -589,7 +614,7 @@ impl Default for ClientOptions { Self { program: CliProgram::Resolve, prefix_args: Vec::new(), - working_directory: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), + working_directory: PathBuf::new(), env: Vec::new(), env_remove: Vec::new(), extra_args: Vec::new(), @@ -632,7 +657,7 @@ impl ClientOptions { Self::default() } - /// How to locate the CLI binary. See [`CliProgram`]. + /// How to locate the child-process runtime. See [`CliProgram`]. pub fn with_program(mut self, program: impl Into) -> Self { self.program = program.into(); self @@ -850,6 +875,85 @@ fn generate_connection_token() -> String { hex } +/// Environment variable that overrides the transport used when the caller +/// leaves [`ClientOptions::transport`] at [`Transport::Default`]. +/// Accepts `"inprocess"` or `"stdio"` (case-insensitive); unset preserves +/// stdio. Any other value is an error. +const DEFAULT_CONNECTION_ENV_VAR: &str = "COPILOT_SDK_DEFAULT_CONNECTION"; + +/// Resolve a transport override from [`DEFAULT_CONNECTION_ENV_VAR`]. +fn resolve_default_transport(options: &ClientOptions) -> Result { + let configured = options + .env + .iter() + .find(|(key, _)| { + key.to_string_lossy() + .eq_ignore_ascii_case(DEFAULT_CONNECTION_ENV_VAR) + }) + .map(|(_, value)| value.to_string_lossy().into_owned()); + let process = std::env::var(DEFAULT_CONNECTION_ENV_VAR).ok(); + resolve_default_transport_value(configured.as_deref().or(process.as_deref())) +} + +fn resolve_default_transport_value(value: Option<&str>) -> Result { + match value { + None => Ok(Transport::Stdio), + Some(v) if v.is_empty() || v.eq_ignore_ascii_case("stdio") => Ok(Transport::Stdio), + Some(v) if v.eq_ignore_ascii_case("inprocess") => Ok(Transport::InProcess), + Some(v) => Err(Error::with_message( + ErrorKind::InvalidConfig, + format!( + "invalid {DEFAULT_CONNECTION_ENV_VAR} value '{v}'. \ + Expected 'inprocess', 'stdio', or unset." + ), + )), + } +} + +#[cfg(any(feature = "bundled-in-process", test))] +fn validate_inprocess_options(options: &ClientOptions) -> Result<()> { + if !matches!(&options.program, CliProgram::Resolve) { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + "ClientOptions::program is not supported with Transport::InProcess; \ + set COPILOT_CLI_PATH only when using an externally provisioned runtime package", + )); + } + if !options.extra_args.is_empty() { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + "ClientOptions::extra_args is not supported with Transport::InProcess; \ + use typed client options instead", + )); + } + + let unsupported = if !options.working_directory.as_os_str().is_empty() { + Some("working_directory") + } else if !options.env.is_empty() { + Some("env") + } else if !options.env_remove.is_empty() { + Some("env_remove") + } else if options.telemetry.is_some() { + Some("telemetry") + } else if !options.prefix_args.is_empty() { + Some("prefix_args") + } else { + None + }; + + if let Some(option) = unsupported { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + format!( + "ClientOptions::{option} is not supported with Transport::InProcess; \ + configure process-global settings on the host process instead" + ), + )); + } + + Ok(()) +} + /// Connection to a GitHub Copilot CLI server (stdio, TCP, or external). /// /// Cheaply cloneable — cloning shares the underlying connection. @@ -870,6 +974,10 @@ impl std::fmt::Debug for Client { struct ClientInner { child: 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. + ffi_host: parking_lot::Mutex>>, rpc: JsonRpcClient, cwd: PathBuf, request_rx: parking_lot::Mutex>>, @@ -916,6 +1024,21 @@ impl Client { /// backend. pub async fn start(options: ClientOptions) -> Result { let start_time = Instant::now(); + let mut options = options; + if matches!(options.transport, Transport::Default) { + options.transport = resolve_default_transport(&options)?; + } + if matches!(options.transport, Transport::InProcess) { + #[cfg(not(feature = "bundled-in-process"))] + { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + "Transport::InProcess requires the `bundled-in-process` Cargo feature", + )); + } + #[cfg(feature = "bundled-in-process")] + validate_inprocess_options(&options)?; + } if options.mode == ClientMode::Empty && options.base_directory.is_none() && options.session_fs.is_none() @@ -970,9 +1093,9 @@ impl Client { // to the server. For Tcp, the SDK auto-generates one when the // caller leaves it unset so the loopback listener is safe by // default. - let mut options = options; let effective_connection_token: Option = match &mut options.transport { - Transport::Stdio => None, + Transport::Default => unreachable!("default transport resolved above"), + Transport::Stdio | Transport::InProcess => None, Transport::Tcp { connection_token, .. } => Some( @@ -1016,8 +1139,17 @@ impl Client { resolved } }; + let working_directory = { + let cwd = options.working_directory.clone(); + if cwd.as_os_str().is_empty() { + std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) + } else { + cwd + } + }; let client = match options.transport { + Transport::Default => unreachable!("default transport resolved above"), Transport::External { ref host, port, @@ -1037,7 +1169,7 @@ impl Client { reader, writer, None, - options.working_directory, + working_directory, options.on_list_models, session_fs_config.is_some(), session_fs_sqlite_declared, @@ -1051,7 +1183,8 @@ impl Client { port, connection_token: _, } => { - let (mut child, actual_port) = Self::spawn_tcp(&program, &options, port).await?; + let (mut child, actual_port) = + Self::spawn_tcp(&program, &options, &working_directory, port).await?; let connect_start = Instant::now(); let stream = TcpStream::connect(("127.0.0.1", actual_port)).await?; debug!( @@ -1065,7 +1198,7 @@ impl Client { reader, writer, Some(child), - options.working_directory, + working_directory, options.on_list_models, session_fs_config.is_some(), session_fs_sqlite_declared, @@ -1076,7 +1209,7 @@ impl Client { )? } Transport::Stdio => { - let mut child = Self::spawn_stdio(&program, &options)?; + let mut child = Self::spawn_stdio(&program, &options, &working_directory)?; let stdin = child.stdin.take().expect("stdin is piped"); let stdout = child.stdout.take().expect("stdout is piped"); Self::drain_stderr(&mut child); @@ -1084,7 +1217,7 @@ impl Client { stdout, stdin, Some(child), - options.working_directory, + working_directory, options.on_list_models, session_fs_config.is_some(), session_fs_sqlite_declared, @@ -1094,8 +1227,69 @@ impl Client { options.mode, )? } + Transport::InProcess => { + #[cfg(feature = "bundled-in-process")] + { + info!(runtime_path = %program.display(), "hosting copilot runtime in-process (FFI)"); + let mut environment = Vec::new(); + if let Some(base_directory) = &options.base_directory { + let value = base_directory.to_str().ok_or_else(|| { + Error::with_message( + ErrorKind::InvalidConfig, + "base_directory must be valid UTF-8 for Transport::InProcess", + ) + })?; + environment.push(("COPILOT_HOME".to_string(), value.to_string())); + } + if options.mode == ClientMode::Empty { + environment.push(("COPILOT_DISABLE_KEYTAR".to_string(), "1".to_string())); + } + if let Some(github_token) = &options.github_token { + environment + .push(("COPILOT_SDK_AUTH_TOKEN".to_string(), github_token.clone())); + } + let mut args = Vec::new(); + args.extend( + Self::log_level_args(&options) + .into_iter() + .map(str::to_string), + ); + args.extend(Self::session_idle_timeout_args(&options)); + args.extend(Self::remote_args(&options)); + if options.github_token.is_some() { + args.extend([ + "--auth-token-env".to_string(), + "COPILOT_SDK_AUTH_TOKEN".to_string(), + ]); + } + let use_logged_in_user = options + .use_logged_in_user + .unwrap_or(options.github_token.is_none()); + if !use_logged_in_user { + args.push("--no-auto-login".to_string()); + } + let host = crate::ffi::FfiHost::create(&program, environment, args)?; + let (reader, writer, shared) = host.start().await?; + let client = Self::from_transport( + reader, + writer, + None, + working_directory, + options.on_list_models, + session_fs_config.is_some(), + session_fs_sqlite_declared, + options.on_get_trace_context, + options.on_github_telemetry, + effective_connection_token.clone(), + options.mode, + )?; + *client.inner.ffi_host.lock() = Some(shared); + client + } + #[cfg(not(feature = "bundled-in-process"))] + unreachable!("in-process feature validation returned above") + } }; - debug!( elapsed_ms = start_time.elapsed().as_millis(), "Client::start transport setup complete" @@ -1294,6 +1488,8 @@ impl Client { let client = Self { inner: Arc::new(ClientInner { child: parking_lot::Mutex::new(child), + #[cfg(feature = "bundled-in-process")] + ffi_host: parking_lot::Mutex::new(None), rpc, cwd, request_rx: parking_lot::Mutex::new(Some(request_rx)), @@ -1362,7 +1558,7 @@ impl Client { }); } - fn build_command(program: &Path, options: &ClientOptions) -> Command { + fn build_command(program: &Path, options: &ClientOptions, working_directory: &Path) -> Command { let mut command = Command::new(program); for arg in &options.prefix_args { command.arg(arg); @@ -1420,7 +1616,7 @@ impl Client { command.env_remove(key); } command - .current_dir(&options.working_directory) + .current_dir(working_directory) .stdout(Stdio::piped()) .stderr(Stdio::piped()); @@ -1483,9 +1679,13 @@ impl Client { } } - fn spawn_stdio(program: &Path, options: &ClientOptions) -> Result { - info!(cwd = ?options.working_directory, program = %program.display(), "spawning copilot CLI (stdio)"); - let mut command = Self::build_command(program, options); + fn spawn_stdio( + program: &Path, + options: &ClientOptions, + working_directory: &Path, + ) -> Result { + info!(cwd = ?working_directory, program = %program.display(), "spawning copilot CLI (stdio)"); + let mut command = Self::build_command(program, options, working_directory); command .args(["--server", "--stdio", "--no-auto-update"]) .args(Self::log_level_args(options)) @@ -1503,9 +1703,14 @@ impl Client { Ok(child) } - async fn spawn_tcp(program: &Path, options: &ClientOptions, port: u16) -> Result<(Child, u16)> { - info!(cwd = ?options.working_directory, program = %program.display(), port = %port, "spawning copilot CLI (tcp)"); - let mut command = Self::build_command(program, options); + async fn spawn_tcp( + program: &Path, + options: &ClientOptions, + working_directory: &Path, + port: u16, + ) -> Result<(Child, u16)> { + info!(cwd = ?working_directory, program = %program.display(), port = %port, "spawning copilot CLI (tcp)"); + let mut command = Self::build_command(program, options, working_directory); command .args(["--server", "--port", &port.to_string(), "--no-auto-update"]) .args(Self::log_level_args(options)) @@ -2064,6 +2269,9 @@ impl Client { } let should_shutdown_runtime = self.inner.child.lock().is_some(); + #[cfg(feature = "bundled-in-process")] + let should_shutdown_runtime = + should_shutdown_runtime || self.inner.ffi_host.lock().is_some(); if should_shutdown_runtime { let runtime_shutdown_start = Instant::now(); match tokio::time::timeout(RUNTIME_SHUTDOWN_TIMEOUT, self.rpc().runtime().shutdown()) @@ -2120,6 +2328,16 @@ impl Client { } } + // The runtime.shutdown RPC above already asked the runtime to clean up; + // closing here tears down the transport. + #[cfg(feature = "bundled-in-process")] + { + if let Some(host) = self.inner.ffi_host.lock().take() { + self.inner.rpc.force_close(); + host.close(); + } + } + info!(pid = ?pid, errors = errors.len(), "CLI process stopped"); if errors.is_empty() { Ok(()) @@ -2166,6 +2384,12 @@ impl Client { error!(pid = ?pid, error = %e, "failed to send kill signal"); } self.inner.rpc.force_close(); + #[cfg(feature = "bundled-in-process")] + { + if let Some(host) = self.inner.ffi_host.lock().take() { + host.close(); + } + } // Drop all session channels so any awaiters see a closed channel // instead of waiting for responses that will never arrive. self.inner.router.clear(); @@ -2222,6 +2446,13 @@ impl Drop for ClientInner { info!(pid = ?pid, "kill signal sent for CLI process on drop"); } } + #[cfg(feature = "bundled-in-process")] + { + if let Some(host) = self.ffi_host.lock().take() { + self.rpc.force_close(); + host.close(); + } + } } } @@ -2286,6 +2517,63 @@ mod tests { assert!(opts.enable_remote_sessions); } + #[test] + fn default_transport_values_resolve_without_process_state() { + assert!(matches!( + resolve_default_transport_value(None).unwrap(), + Transport::Stdio + )); + assert!(matches!( + resolve_default_transport_value(Some("stdio")).unwrap(), + Transport::Stdio + )); + assert!(matches!( + resolve_default_transport_value(Some("INPROCESS")).unwrap(), + Transport::InProcess + )); + assert!(resolve_default_transport_value(Some("tcp")).is_err()); + } + + #[test] + fn inprocess_rejects_process_scoped_options() { + let invalid = [ + ClientOptions::new().with_cwd("."), + ClientOptions::new().with_env([("KEY", "value")]), + ClientOptions::new().with_env_remove(["KEY"]), + ClientOptions::new().with_telemetry(TelemetryConfig::default()), + ClientOptions::new().with_prefix_args(["index.js"]), + ClientOptions::new().with_program(CliProgram::Path("copilot".into())), + ClientOptions::new().with_extra_args(["--verbose"]), + ]; + + for options in invalid { + assert!(validate_inprocess_options(&options).is_err()); + } + } + + #[test] + fn inprocess_allows_typed_runtime_options() { + let options = ClientOptions::new() + .with_base_directory("state") + .with_log_level(LogLevel::Debug) + .with_session_idle_timeout_seconds(10) + .with_github_token("token") + .with_use_logged_in_user(false) + .with_enable_remote_sessions(true); + + assert!(validate_inprocess_options(&options).is_ok()); + } + + #[cfg(not(feature = "bundled-in-process"))] + #[tokio::test] + async fn inprocess_requires_cargo_feature() { + let error = Client::start(ClientOptions::new().with_transport(Transport::InProcess)) + .await + .unwrap_err(); + + assert!(error.to_string().contains("bundled-in-process")); + } + #[test] fn is_transport_failure_rejects_other_protocol_errors() { let err = Error::from(ErrorKind::Protocol(ProtocolErrorKind::CliStartupTimeout)); @@ -2299,7 +2587,7 @@ mod tests { env_remove: vec![std::ffi::OsString::from("COPILOT_SDK_AUTH_TOKEN")], ..Default::default() }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); // get_envs() iter yields the latest action per key — None means removed. let action = cmd .as_std() @@ -2323,7 +2611,7 @@ mod tests { )], ..Default::default() }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); let value = cmd .as_std() .get_envs() @@ -2338,7 +2626,7 @@ mod tests { github_token: Some("just-the-token".to_string()), ..Default::default() }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); let value = cmd .as_std() .get_envs() @@ -2406,7 +2694,7 @@ mod tests { }), ..Default::default() }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); assert_eq!( env_value(&cmd, "COPILOT_OTEL_ENABLED"), Some(std::ffi::OsStr::new("true")), @@ -2440,7 +2728,7 @@ mod tests { #[test] fn build_command_omits_otel_env_when_telemetry_none() { let opts = ClientOptions::default(); - let cmd = Client::build_command(Path::new("/bin/echo"), &opts); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); for key in [ "COPILOT_OTEL_ENABLED", "OTEL_EXPORTER_OTLP_ENDPOINT", @@ -2466,7 +2754,7 @@ mod tests { }), ..Default::default() }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); // The one set field plus the implicit enabled flag should propagate. assert_eq!( env_value(&cmd, "COPILOT_OTEL_ENABLED"), @@ -2501,7 +2789,7 @@ mod tests { )], ..Default::default() }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); assert_eq!( env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"), Some(std::ffi::OsStr::new("http://from-user-env:4318")), @@ -2512,14 +2800,14 @@ mod tests { #[test] fn build_command_sets_copilot_home_env_when_configured() { let opts = ClientOptions::new().with_base_directory(PathBuf::from("/custom/copilot")); - let cmd = Client::build_command(Path::new("/bin/echo"), &opts); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); assert_eq!( env_value(&cmd, "COPILOT_HOME"), Some(std::ffi::OsStr::new("/custom/copilot")), ); let opts = ClientOptions::default(); - let cmd = Client::build_command(Path::new("/bin/echo"), &opts); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); assert!(env_value(&cmd, "COPILOT_HOME").is_none()); } @@ -2529,14 +2817,14 @@ mod tests { port: 0, connection_token: Some("secret-token".to_string()), }); - let cmd = Client::build_command(Path::new("/bin/echo"), &opts); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); assert_eq!( env_value(&cmd, "COPILOT_CONNECTION_TOKEN"), Some(std::ffi::OsStr::new("secret-token")), ); let opts = ClientOptions::default(); - let cmd = Client::build_command(Path::new("/bin/echo"), &opts); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); assert!(env_value(&cmd, "COPILOT_CONNECTION_TOKEN").is_none()); } @@ -2587,8 +2875,9 @@ mod tests { }), ..Default::default() }; - let cmd_true = Client::build_command(Path::new("/bin/echo"), &opts_true); - let cmd_false = Client::build_command(Path::new("/bin/echo"), &opts_false); + let cmd_true = Client::build_command(Path::new("/bin/echo"), &opts_true, Path::new("/tmp")); + let cmd_false = + Client::build_command(Path::new("/bin/echo"), &opts_false, Path::new("/tmp")); assert_eq!( env_value( &cmd_true, @@ -2798,6 +3087,8 @@ mod tests { Client { inner: Arc::new(ClientInner { child: parking_lot::Mutex::new(None), + #[cfg(feature = "bundled-in-process")] + ffi_host: parking_lot::Mutex::new(None), rpc: { let (req_tx, _req_rx) = mpsc::unbounded_channel(); let (notif_tx, _notif_rx) = broadcast::channel(16); diff --git a/rust/src/session.rs b/rust/src/session.rs index d0fadd0449..35656c657f 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -12,7 +12,8 @@ use tracing::{Instrument, warn}; use crate::canvas::CanvasHandler; use crate::generated::api_types::{ - LogRequest, ModelSwitchToRequest, OpenCanvasInstance, RegisterEventInterestParams, rpc_methods, + LogRequest, ModelSwitchToRequest, OpenCanvasInstance, RegisterEventInterestParams, + ToolsGetCurrentMetadataResult, rpc_methods, }; use crate::generated::session_events::{ CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, McpOauthRequiredData, @@ -41,6 +42,11 @@ use crate::{ error_codes, }; +/// Fixed name of the runtime's built-in tool-search tool. A client can replace +/// its behavior by registering a tool with this exact name and +/// `overrides_built_in_tool` set to `true`. +const TOOL_SEARCH_TOOL_NAME: &str = "tool_search_tool"; + /// Bundle of the per-session callbacks the SDK dispatches to. Built from a /// [`SessionConfig`] / [`ResumeSessionConfig`] at /// [`Client::create_session`] / [`Client::resume_session`] time. Each @@ -529,6 +535,7 @@ impl Session { model_id: model.to_string(), reasoning_effort: opts.reasoning_effort, reasoning_summary: opts.reasoning_summary, + verbosity: None, context_tier: opts.context_tier, model_capabilities: opts.model_capabilities, }; @@ -1515,6 +1522,7 @@ fn tool_failure_result(message: impl Into) -> ToolResult { session_log: None, error: Some(message), tool_telemetry: None, + tool_references: None, }) } @@ -1806,6 +1814,30 @@ async fn handle_notification( } let tool_call_id = data.tool_call_id.clone(); let tool_name = data.tool_name.clone(); + // The built-in tool-search tool receives a snapshot of the + // session's currently initialized tools so an override can + // filter the live catalog without issuing its own RPC. Fetch + // it only for that tool to avoid a round-trip on every tool + // 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( + rpc_methods::SESSION_TOOLS_GETCURRENTMETADATA, + Some(serde_json::json!({ "sessionId": sid })), + ) + .await + { + Ok(value) => { + serde_json::from_value::(value) + .ok() + .and_then(|result| result.tools) + } + Err(_) => None, + } + } else { + None + }; let invocation = ToolInvocation { session_id: sid.clone(), tool_call_id: data.tool_call_id, @@ -1813,6 +1845,7 @@ async fn handle_notification( arguments: data .arguments .unwrap_or(Value::Object(serde_json::Map::new())), + available_tools, traceparent: data.traceparent, tracestate: data.tracestate, }; diff --git a/rust/src/tool.rs b/rust/src/tool.rs index 189bc6f21a..344d2894ca 100644 --- a/rust/src/tool.rs +++ b/rust/src/tool.rs @@ -13,9 +13,8 @@ //! Enable the `derive` feature for `schema_for`, which generates JSON //! Schema from Rust types via `schemars`. -use std::collections::HashMap; - use async_trait::async_trait; +use indexmap::IndexMap; /// Re-export of [`schemars::JsonSchema`] for deriving tool parameter schemas. #[cfg(feature = "derive")] pub use schemars::JsonSchema; @@ -80,14 +79,14 @@ pub fn schema_for() -> serde_json::Value { /// tool.parameters = tool_parameters(serde_json::json!({"type": "object"})); /// # let _ = tool; /// ``` -pub fn tool_parameters(schema: serde_json::Value) -> HashMap { +pub fn tool_parameters(schema: serde_json::Value) -> IndexMap { try_tool_parameters(schema).expect("tool parameter schema must be a JSON object") } /// Fallible variant of [`tool_parameters`] for callers handling dynamic schema input. pub fn try_tool_parameters( schema: serde_json::Value, -) -> Result, serde_json::Error> { +) -> Result, serde_json::Error> { serde_json::from_value(schema) } @@ -174,6 +173,7 @@ pub fn convert_mcp_call_tool_result(value: &serde_json::Value) -> Option = tool.parameters.keys().map(String::as_str).collect(); + assert_eq!( + keys, + ["additionalProperties", "properties", "required", "type"] + ); + } + #[test] fn convert_mcp_call_tool_result_collects_text_and_binary_content() { let result = convert_mcp_call_tool_result(&serde_json::json!({ @@ -566,6 +604,7 @@ mod tests { tool_call_id: "tc1".to_string(), tool_name: "echo".to_string(), arguments: serde_json::json!({"msg": "hello"}), + available_tools: None, traceparent: None, tracestate: None, }; @@ -606,6 +645,7 @@ mod tests { tool_call_id: "tc1".to_string(), tool_name: "weather".to_string(), arguments: serde_json::json!({"city": "Seattle"}), + available_tools: None, traceparent: None, tracestate: None, }; @@ -688,6 +728,7 @@ mod tests { tool_call_id: "tc1".to_string(), tool_name: "get_weather".to_string(), arguments: serde_json::json!({"city": "Seattle", "unit": "celsius"}), + available_tools: None, traceparent: None, tracestate: None, }; @@ -707,6 +748,7 @@ mod tests { tool_call_id: "tc1".to_string(), tool_name: "get_weather".to_string(), arguments: serde_json::json!({"wrong_field": 42}), + available_tools: None, traceparent: None, tracestate: None, }; @@ -728,6 +770,7 @@ mod tests { tool_call_id: "tc1".to_string(), tool_name: "get_weather".to_string(), arguments: serde_json::json!({"city": "Portland"}), + available_tools: None, traceparent: None, tracestate: None, }) diff --git a/rust/src/types.rs b/rust/src/types.rs index e5ba28a56e..028702655f 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -9,6 +9,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; +use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -19,7 +20,7 @@ pub use crate::copilot_request_handler::{ CopilotWebSocketForwarderBuilder, CopilotWebSocketHandler, CopilotWebSocketMessage, CopilotWebSocketResponse, WebSocketTransform, forward_http, }; -use crate::generated::api_types::OpenCanvasInstance; +use crate::generated::api_types::{CurrentToolMetadata, OpenCanvasInstance}; use crate::generated::session_events::ReasoningSummary; /// Context window tier for models that support tiered context windows. pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig}; @@ -332,8 +333,8 @@ pub struct Tool { #[serde(default, skip_serializing_if = "Option::is_none")] pub instructions: Option, /// JSON Schema for the tool's input parameters. - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub parameters: HashMap, + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub parameters: IndexMap, /// When `true`, this tool replaces a built-in tool of the same name /// (e.g. supplying a custom `grep` that the agent uses in place of the /// CLI's built-in implementation). @@ -351,6 +352,12 @@ pub struct Tool { /// runtime decide. #[serde(default, skip_serializing_if = "Option::is_none")] pub defer: Option, + /// Opaque, host-defined metadata associated with the tool definition. + /// Keys are namespaced and not part of the stable public API; values are + /// not interpreted and may be recognized to inform host-specific behavior. + /// Unknown keys are preserved and round-tripped untouched. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub metadata: IndexMap, /// Optional runtime implementation. When `Some`, the SDK dispatches /// matching `external_tool.requested` broadcasts to this handler. /// When `None`, the tool is declaration-only. @@ -470,6 +477,13 @@ impl Tool { self } + /// Set opaque, host-defined metadata for the tool. Keys are namespaced and + /// not part of the stable public API. Replaces any previously-set metadata. + pub fn with_metadata(mut self, metadata: IndexMap) -> Self { + self.metadata = metadata; + self + } + /// Attach a runtime implementation. The SDK will dispatch matching /// `external_tool.requested` broadcasts to `handler` for this tool's /// name. Without a handler the tool is declaration-only. @@ -498,6 +512,7 @@ impl std::fmt::Debug for Tool { .field("overrides_built_in_tool", &self.overrides_built_in_tool) .field("skip_permission", &self.skip_permission) .field("defer", &self.defer) + .field("metadata", &self.metadata) .field( "handler", &self.handler.as_ref().map(|_| "").unwrap_or("None"), @@ -614,7 +629,7 @@ pub struct CustomAgentConfig { pub prompt: String, /// MCP servers specific to this agent. #[serde(default, skip_serializing_if = "Option::is_none")] - pub mcp_servers: Option>, + pub mcp_servers: Option>, /// Whether the agent is available for model inference. #[serde(default, skip_serializing_if = "Option::is_none")] pub infer: Option, @@ -627,6 +642,12 @@ pub struct CustomAgentConfig { /// falling back to the parent session model if unavailable. #[serde(default, skip_serializing_if = "Option::is_none")] pub model: Option, + /// Reasoning effort level for this agent's model. + /// + /// When unset, no per-agent override is sent and the backend chooses its + /// default. The parent session effort is not inherited. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, } impl CustomAgentConfig { @@ -668,7 +689,7 @@ impl CustomAgentConfig { } /// Configure agent-specific MCP servers. - pub fn with_mcp_servers(mut self, mcp_servers: HashMap) -> Self { + pub fn with_mcp_servers(mut self, mcp_servers: IndexMap) -> Self { self.mcp_servers = Some(mcp_servers); self } @@ -694,6 +715,12 @@ impl CustomAgentConfig { self.model = Some(model.into()); self } + + /// Set the reasoning effort level for this agent's model. + pub fn with_reasoning_effort(mut self, reasoning_effort: impl Into) -> Self { + self.reasoning_effort = Some(reasoning_effort.into()); + self + } } /// Configures the default (built-in) agent that handles turns when no @@ -758,6 +785,45 @@ impl LargeToolOutputConfig { } } +/// Overrides the runtime's built-in tool-search behavior. +/// +/// Tool search defers tools to keep the model's active tool set small. +/// To override the tool-search tool's implementation, register a [`Tool`] +/// named `"tool_search_tool"` with [`Tool::overrides_built_in_tool`] set to `true`. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct ToolSearchConfig { + /// Toggle to enable/disable tool search. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, + /// The tool count above which MCP and external tools are deferred behind + /// tool search. When unset, the runtime default (30) applies. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub defer_threshold: Option, +} + +impl ToolSearchConfig { + /// Construct an empty [`ToolSearchConfig`]; all fields default to unset + /// (the runtime applies its own defaults). + pub fn new() -> Self { + Self::default() + } + + /// Toggle that enables or disables tool search. + pub fn with_enabled(mut self, enabled: bool) -> Self { + self.enabled = Some(enabled); + self + } + + /// Set the tool count above which MCP and external tools are deferred + /// behind tool search. + pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self { + self.defer_threshold = Some(defer_threshold); + self + } +} + /// Configures infinite sessions: persistent workspaces with automatic /// context-window compaction. /// @@ -916,6 +982,42 @@ impl ExtensionInfo { } } +/// Stable identity for a host/SDK connection that supplies built-in canvases. +/// +/// When set on session create or resume, the runtime uses [`id`] verbatim as +/// the agent-facing canvas extension id, so canvases declared on a control +/// connection survive stdio reconnect and CLI process restart instead of being +/// re-keyed to a per-connection id. The id is opaque to the runtime; a +/// per-window-stable value such as `app:builtin:` is recommended. An +/// id beginning with `connection:` is reserved and ignored by the runtime. +/// +/// [`id`]: CanvasProviderIdentity::id +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct CanvasProviderIdentity { + /// Opaque, stable provider id used verbatim as the canvas extension id. + pub id: String, + /// Optional display name surfaced as the canvas extension name. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +impl CanvasProviderIdentity { + /// Create a canvas provider identity from a stable opaque id. + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + name: None, + } + } + + /// Set the optional display name surfaced as the canvas extension name. + pub fn with_name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } +} + /// Configuration for a single MCP server. /// /// MCP (Model Context Protocol) servers expose external tools to the @@ -929,8 +1031,8 @@ impl ExtensionInfo { /// /// ``` /// # use github_copilot_sdk::types::{McpServerConfig, McpStdioServerConfig, McpHttpServerConfig}; -/// # use std::collections::HashMap; -/// let mut servers = HashMap::new(); +/// # use github_copilot_sdk::IndexMap; +/// let mut servers = IndexMap::new(); /// servers.insert( /// "playwright".to_string(), /// McpServerConfig::Stdio(McpStdioServerConfig { @@ -1598,6 +1700,9 @@ pub struct SessionConfig { pub extension_sdk_path: Option, /// Stable extension identity for canvas/tool providers on this connection. pub extension_info: Option, + /// Stable identity for a host/SDK connection that supplies built-in + /// canvases, so they survive reconnect and CLI restart. + pub canvas_provider: Option, /// Allowlist of built-in tool names the agent may use. pub available_tools: Option>, /// Blocklist of built-in tool names the agent must not use. @@ -1609,7 +1714,7 @@ pub struct SessionConfig { /// configured. pub excluded_builtin_agents: Option>, /// MCP server configurations passed through to the CLI. - pub mcp_servers: Option>, + pub mcp_servers: Option>, /// Controls how MCP OAuth tokens are stored for this session. /// /// - `"persistent"` — tokens are stored in the OS keychain (shared across sessions). @@ -1674,6 +1779,10 @@ pub struct SessionConfig { pub plugin_directories: Option>, /// Configuration for large tool output handling, forwarded to the CLI. pub large_output: Option, + /// Overrides the runtime's built-in tool-search behavior, which defers + /// rarely used tools behind a searchable index. When unset, the runtime + /// default applies. + pub tool_search: Option, /// Skill names to disable. Skills in this set will not be available /// even if found in skill directories. pub disabled_skills: Option>, @@ -1770,6 +1879,13 @@ pub struct SessionConfig { /// [`with_exp_assignments`](Self::with_exp_assignments). #[doc(hidden)] pub exp_assignments: Option, + /// Opt-in: when `Some(true)`, the runtime self-fetches enterprise managed + /// settings (bypass-permissions policy) at session bootstrap using the + /// session's [`github_token`](Self::github_token). Requires `github_token` + /// to be set; if omitted, the runtime is expected to reject session creation + /// (fail-closed). When `None`, behaves exactly as before. Set via + /// [`with_enable_managed_settings`](Self::with_enable_managed_settings). + pub enable_managed_settings: Option, /// Custom session filesystem provider for this session. Required when /// the [`Client`](crate::Client) was started with /// [`ClientOptions::session_fs`](crate::ClientOptions::session_fs) set. @@ -1847,6 +1963,7 @@ impl std::fmt::Debug for SessionConfig { .field("request_extensions", &self.request_extensions) .field("extension_sdk_path", &self.extension_sdk_path) .field("extension_info", &self.extension_info) + .field("canvas_provider", &self.canvas_provider) .field("available_tools", &self.available_tools) .field("excluded_tools", &self.excluded_tools) .field("excluded_builtin_agents", &self.excluded_builtin_agents) @@ -1878,6 +1995,7 @@ impl std::fmt::Debug for SessionConfig { .field("instruction_directories", &self.instruction_directories) .field("plugin_directories", &self.plugin_directories) .field("large_output", &self.large_output) + .field("tool_search", &self.tool_search) .field("disabled_skills", &self.disabled_skills) .field("hooks", &self.hooks) .field("custom_agents", &self.custom_agents) @@ -1905,6 +2023,7 @@ impl std::fmt::Debug for SessionConfig { ) .field("commands", &self.commands) .field("exp_assignments", &self.exp_assignments) + .field("enable_managed_settings", &self.enable_managed_settings) .field( "session_fs_provider", &self.session_fs_provider.as_ref().map(|_| ""), @@ -1968,6 +2087,7 @@ impl Default for SessionConfig { request_extensions: None, extension_sdk_path: None, extension_info: None, + canvas_provider: None, available_tools: None, excluded_tools: None, excluded_builtin_agents: None, @@ -1987,6 +2107,7 @@ impl Default for SessionConfig { instruction_directories: None, plugin_directories: None, large_output: None, + tool_search: None, disabled_skills: None, hooks: None, custom_agents: None, @@ -2010,6 +2131,7 @@ impl Default for SessionConfig { include_sub_agent_streaming_events: None, commands: None, exp_assignments: None, + enable_managed_settings: None, session_fs_provider: None, permission_handler: None, elicitation_handler: None, @@ -2057,7 +2179,7 @@ impl SessionConfig { /// /// Wire-format flags are derived from handler presence and the policy /// field; runtime fields are moved out into the returned runtime so - /// the deep `Vec` / `HashMap` clones the previous + /// the deep `Vec` / `IndexMap` clones the previous /// `&self`-based shape required are eliminated, and the order of /// reading-vs-moving is enforced at compile time. /// @@ -2116,6 +2238,7 @@ impl SessionConfig { request_extensions: self.request_extensions, extension_sdk_path: self.extension_sdk_path, extension_info: self.extension_info, + canvas_provider: self.canvas_provider, available_tools: self.available_tools, excluded_tools: self.excluded_tools, excluded_builtin_agents: self.excluded_builtin_agents, @@ -2143,6 +2266,7 @@ impl SessionConfig { instruction_directories: self.instruction_directories, plugin_directories: self.plugin_directories, large_output: self.large_output, + tool_search: self.tool_search, disabled_skills: self.disabled_skills, custom_agents: self.custom_agents, default_agent: self.default_agent, @@ -2166,6 +2290,7 @@ impl SessionConfig { enable_github_telemetry_forwarding: None, commands: wire_commands, exp_assignments: self.exp_assignments, + enable_managed_settings: self.enable_managed_settings, }; let runtime = SessionConfigRuntime { @@ -2390,6 +2515,13 @@ impl SessionConfig { self } + /// Set the canvas provider identity for this connection so host-supplied + /// canvases survive reconnect and CLI restart. + pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self { + self.canvas_provider = Some(canvas_provider); + self + } + /// Set the allowlist of built-in tool names the agent may use. pub fn with_available_tools(mut self, tools: I) -> Self where @@ -2421,7 +2553,7 @@ impl SessionConfig { } /// Set MCP server configurations passed through to the CLI. - pub fn with_mcp_servers(mut self, servers: HashMap) -> Self { + pub fn with_mcp_servers(mut self, servers: IndexMap) -> Self { self.mcp_servers = Some(servers); self } @@ -2546,6 +2678,13 @@ impl SessionConfig { self } + /// Set the [`ToolSearchConfig`] overriding the runtime's built-in + /// tool-search behavior on session create. + pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self { + self.tool_search = Some(config); + self + } + /// Set the names of skills to disable (overrides skill discovery). pub fn with_disabled_skills(mut self, names: I) -> Self where @@ -2732,6 +2871,16 @@ impl SessionConfig { self.exp_assignments = Some(assignments); self } + + /// Opt the runtime into self-fetching enterprise managed settings + /// (bypass-permissions policy) at session bootstrap using the session's + /// [`github_token`](Self::github_token). Requires `github_token` to be set; + /// if omitted, the runtime is expected to reject session creation + /// (fail-closed). + pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self { + self.enable_managed_settings = Some(enabled); + self + } } /// /// See [`SessionConfig`] for the construction patterns (chained `with_*` @@ -2782,6 +2931,9 @@ pub struct ResumeSessionConfig { pub extension_sdk_path: Option, /// Stable extension identity for canvas/tool providers on this connection. pub extension_info: Option, + /// Stable identity for a host/SDK connection that supplies built-in + /// canvases, so they rehydrate against a stable extension id on resume. + pub canvas_provider: Option, /// Allowlist of tool names the agent may use. pub available_tools: Option>, /// Blocklist of built-in tool names. @@ -2793,7 +2945,7 @@ pub struct ResumeSessionConfig { /// configured. pub excluded_builtin_agents: Option>, /// Re-supply MCP servers so they remain available after app restart. - pub mcp_servers: Option>, + pub mcp_servers: Option>, /// Controls how MCP OAuth tokens are stored for this session. /// See [`SessionConfig::mcp_oauth_token_storage`] for details. pub mcp_oauth_token_storage: Option, @@ -2830,6 +2982,9 @@ pub struct ResumeSessionConfig { pub plugin_directories: Option>, /// Configuration for large tool output handling, forwarded to the CLI on resume. pub large_output: Option, + /// Overrides the runtime's built-in tool-search behavior on resume. When + /// unset, the runtime default applies. + pub tool_search: Option, /// Skill names to disable on resume. pub disabled_skills: Option>, /// Enable session hooks on resume. @@ -2900,6 +3055,12 @@ pub struct ResumeSessionConfig { /// [`with_exp_assignments`](Self::with_exp_assignments). #[doc(hidden)] pub exp_assignments: Option, + /// Opt-in flag injected on resume. See + /// [`SessionConfig::enable_managed_settings`]. Re-supply on resume so + /// the runtime re-applies the managed-settings self-fetch after a CLI + /// process restart. Set via + /// [`with_enable_managed_settings`](Self::with_enable_managed_settings). + pub enable_managed_settings: Option, /// Custom session filesystem provider. Required on resume when the /// [`Client`](crate::Client) was started with /// [`ClientOptions::session_fs`](crate::ClientOptions::session_fs). @@ -2971,6 +3132,7 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("request_extensions", &self.request_extensions) .field("extension_sdk_path", &self.extension_sdk_path) .field("extension_info", &self.extension_info) + .field("canvas_provider", &self.canvas_provider) .field("available_tools", &self.available_tools) .field("excluded_tools", &self.excluded_tools) .field("excluded_builtin_agents", &self.excluded_builtin_agents) @@ -3002,6 +3164,7 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("instruction_directories", &self.instruction_directories) .field("plugin_directories", &self.plugin_directories) .field("large_output", &self.large_output) + .field("tool_search", &self.tool_search) .field("disabled_skills", &self.disabled_skills) .field("hooks", &self.hooks) .field("custom_agents", &self.custom_agents) @@ -3028,6 +3191,7 @@ impl std::fmt::Debug for ResumeSessionConfig { ) .field("commands", &self.commands) .field("exp_assignments", &self.exp_assignments) + .field("enable_managed_settings", &self.enable_managed_settings) .field( "session_fs_provider", &self.session_fs_provider.as_ref().map(|_| ""), @@ -3128,6 +3292,7 @@ impl ResumeSessionConfig { request_extensions: self.request_extensions, extension_sdk_path: self.extension_sdk_path, extension_info: self.extension_info, + canvas_provider: self.canvas_provider, available_tools: self.available_tools, excluded_tools: self.excluded_tools, excluded_builtin_agents: self.excluded_builtin_agents, @@ -3155,6 +3320,7 @@ impl ResumeSessionConfig { instruction_directories: self.instruction_directories, plugin_directories: self.plugin_directories, large_output: self.large_output, + tool_search: self.tool_search, disabled_skills: self.disabled_skills, custom_agents: self.custom_agents, default_agent: self.default_agent, @@ -3177,6 +3343,7 @@ impl ResumeSessionConfig { enable_github_telemetry_forwarding: None, commands: wire_commands, exp_assignments: self.exp_assignments, + enable_managed_settings: self.enable_managed_settings, suppress_resume_event: self.suppress_resume_event, continue_pending_work: self.continue_pending_work, }; @@ -3223,6 +3390,7 @@ impl ResumeSessionConfig { request_extensions: None, extension_sdk_path: None, extension_info: None, + canvas_provider: None, available_tools: None, excluded_tools: None, excluded_builtin_agents: None, @@ -3242,6 +3410,7 @@ impl ResumeSessionConfig { instruction_directories: None, plugin_directories: None, large_output: None, + tool_search: None, disabled_skills: None, hooks: None, custom_agents: None, @@ -3264,6 +3433,7 @@ impl ResumeSessionConfig { include_sub_agent_streaming_events: None, commands: None, exp_assignments: None, + enable_managed_settings: None, session_fs_provider: None, suppress_resume_event: None, continue_pending_work: None, @@ -3474,6 +3644,13 @@ impl ResumeSessionConfig { self } + /// Set the canvas provider identity for this connection on resume so + /// host-supplied canvases rehydrate against a stable extension id. + pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self { + self.canvas_provider = Some(canvas_provider); + self + } + /// Set the allowlist of tool names the agent may use. pub fn with_available_tools(mut self, tools: I) -> Self where @@ -3505,7 +3682,7 @@ impl ResumeSessionConfig { } /// Re-supply MCP server configurations on resume. - pub fn with_mcp_servers(mut self, servers: HashMap) -> Self { + pub fn with_mcp_servers(mut self, servers: IndexMap) -> Self { self.mcp_servers = Some(servers); self } @@ -3625,6 +3802,13 @@ impl ResumeSessionConfig { self } + /// Set the [`ToolSearchConfig`] overriding the runtime's built-in + /// tool-search behavior on resume. + pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self { + self.tool_search = Some(config); + self + } + /// Set the names of skills to disable on resume. pub fn with_disabled_skills(mut self, names: I) -> Self where @@ -3813,6 +3997,13 @@ impl ResumeSessionConfig { self.exp_assignments = Some(assignments); self } + + /// Opt the runtime into self-fetching enterprise managed settings on resume. + /// See [`SessionConfig::with_enable_managed_settings`]. + pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self { + self.enable_managed_settings = Some(enabled); + self + } } /// Controls how the system message is constructed. @@ -4744,6 +4935,15 @@ pub struct ToolInvocation { pub tool_name: String, /// Tool arguments as JSON. pub arguments: Value, + /// Snapshot of the session's currently initialized tools. + /// + /// The SDK populates this only when the invocation targets the built-in + /// tool-search tool (`tool_search_tool`), so a tool-search override can + /// rank/filter the live catalog — including MCP tools configured in + /// settings — without issuing its own RPC. `None` for every other tool + /// invocation. This field is not part of the wire protocol. + #[serde(skip)] + pub available_tools: Option>, /// W3C Trace Context `traceparent` header propagated from the CLI's /// `execute_tool` span. Pass through to OpenTelemetry-aware code so /// child spans created inside the handler are parented to the CLI @@ -4807,8 +5007,14 @@ pub struct ToolBinaryResult { } /// Expanded tool result with metadata for the LLM and session log. +/// +/// This type is `#[non_exhaustive]`: it mirrors a growing wire shape, so +/// construct it via [`ToolResultExpanded::new`] plus the `with_*` chain +/// rather than a struct literal, allowing new fields to land without +/// breaking callers. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +#[non_exhaustive] pub struct ToolResultExpanded { /// Result text sent back to the LLM. pub text_result_for_llm: String, @@ -4826,6 +5032,60 @@ pub struct ToolResultExpanded { /// Tool-specific telemetry emitted with the result. #[serde(default, skip_serializing_if = "Option::is_none")] pub tool_telemetry: Option>, + /// Names of tools returned by a tool-search tool. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tool_references: Option>, +} + +impl ToolResultExpanded { + /// Construct an expanded result with the required `text_result_for_llm` + /// and `result_type` (`"success"` or `"failure"`). All optional metadata + /// fields start unset; populate them with the `with_*` builders. + pub fn new(text_result_for_llm: impl Into, result_type: impl Into) -> Self { + Self { + text_result_for_llm: text_result_for_llm.into(), + result_type: result_type.into(), + binary_results_for_llm: None, + session_log: None, + error: None, + tool_telemetry: None, + tool_references: None, + } + } + + /// Set the binary payloads returned to the LLM. + pub fn with_binary_results(mut self, results: Vec) -> Self { + self.binary_results_for_llm = Some(results); + self + } + + /// Set the log message for the session timeline. + pub fn with_session_log(mut self, session_log: impl Into) -> Self { + self.session_log = Some(session_log.into()); + self + } + + /// Set the error message, marking the tool as failed. + pub fn with_error(mut self, error: impl Into) -> Self { + self.error = Some(error.into()); + self + } + + /// Set the tool-specific telemetry emitted with the result. + pub fn with_tool_telemetry(mut self, telemetry: HashMap) -> Self { + self.tool_telemetry = Some(telemetry); + self + } + + /// Set the names of tools returned by a tool-search tool. + pub fn with_tool_references(mut self, references: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.tool_references = Some(references.into_iter().map(Into::into).collect()); + self + } } /// Result of a tool invocation — either a plain text string or an expanded result. @@ -5168,10 +5428,11 @@ mod tests { AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition, AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState, CustomAgentConfig, DeliveryMode, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig, - LargeToolOutputConfig, MemoryConfiguration, NamedProviderConfig, ProviderConfig, - ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, - SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded, - ToolResultResponse, ensure_attachment_display_names, + LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration, + NamedProviderConfig, ProviderConfig, ProviderModelConfig, ReasoningSummary, + ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, SystemMessageConfig, Tool, + ToolBinaryResult, ToolResult, ToolResultExpanded, ToolResultResponse, + ensure_attachment_display_names, }; use crate::generated::session_events::TypedSessionEvent; @@ -5209,6 +5470,32 @@ mod tests { assert!(value.get("defer").is_none()); } + #[test] + fn tool_metadata_serialization() { + use indexmap::IndexMap; + + let mut metadata = IndexMap::new(); + metadata.insert( + "github.com/copilot:safeForTelemetry".to_string(), + json!({ "name": true, "inputsNames": false }), + ); + let tool = Tool::new("lookup").with_metadata(metadata); + let value = serde_json::to_value(&tool).unwrap(); + assert_eq!( + value + .get("metadata") + .unwrap() + .get("github.com/copilot:safeForTelemetry") + .unwrap(), + &json!({ "name": true, "inputsNames": false }) + ); + + // Empty metadata is omitted on the wire. + let plain = Tool::new("plain"); + let value = serde_json::to_value(&plain).unwrap(); + assert!(value.get("metadata").is_none()); + } + #[test] fn custom_agent_config_builder_with_model() { let agent = CustomAgentConfig::new("my-agent", "You are helpful.") @@ -5234,6 +5521,28 @@ mod tests { assert!(wire.get("model").is_none()); } + #[test] + fn custom_agent_config_builder_with_reasoning_effort() { + let agent = + CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high"); + assert_eq!(agent.reasoning_effort.as_deref(), Some("high")); + } + + #[test] + fn custom_agent_config_serializes_reasoning_effort() { + let agent = + CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high"); + let wire = serde_json::to_value(&agent).unwrap(); + assert_eq!(wire["reasoningEffort"], "high"); + } + + #[test] + fn custom_agent_config_omits_reasoning_effort_when_none() { + let agent = CustomAgentConfig::new("default-agent", "prompt"); + let wire = serde_json::to_value(&agent).unwrap(); + assert!(wire.get("reasoningEffort").is_none()); + } + #[test] #[should_panic(expected = "tool parameter schema must be a JSON object")] fn tool_with_parameters_panics_on_non_object_value() { @@ -5255,6 +5564,7 @@ mod tests { session_log: None, error: None, tool_telemetry: None, + tool_references: None, }), }; @@ -5289,6 +5599,7 @@ mod tests { session_log: None, error: None, tool_telemetry: None, + tool_references: None, }), }; @@ -5298,6 +5609,70 @@ mod tests { assert!(wire["result"].get("binaryResultsForLlm").is_none()); } + #[test] + fn tool_result_expanded_serializes_tool_references() { + let response = ToolResultResponse { + result: ToolResult::Expanded( + ToolResultExpanded::new("found 2 tools", "success") + .with_tool_references(["get_weather", "check_status"]), + ), + }; + + let wire = serde_json::to_value(&response).unwrap(); + + assert_eq!( + wire, + json!({ + "result": { + "textResultForLlm": "found 2 tools", + "resultType": "success", + "toolReferences": ["get_weather", "check_status"] + } + }) + ); + } + + #[test] + fn tool_result_expanded_omits_tool_references_when_none() { + let response = ToolResultResponse { + result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")), + }; + + let wire = serde_json::to_value(&response).unwrap(); + + assert_eq!(wire["result"]["textResultForLlm"], "ok"); + assert!(wire["result"].get("toolReferences").is_none()); + } + + #[test] + fn tool_result_expanded_with_tool_references_accepts_owned_strings() { + // The builder is generic over `Into`, so an owned `Vec` + // must compile and populate the field just like a `&str` array. + let names: Vec = vec!["alpha".to_string(), "beta".to_string()]; + let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names); + + assert_eq!( + expanded.tool_references.as_deref(), + Some(["alpha".to_string(), "beta".to_string()].as_slice()) + ); + } + + #[test] + fn tool_result_expanded_deserializes_tool_references() { + let wire = json!({ + "textResultForLlm": "found tools", + "resultType": "success", + "toolReferences": ["alpha", "beta"] + }); + + let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap(); + + assert_eq!( + expanded.tool_references.as_deref(), + Some(["alpha".to_string(), "beta".to_string()].as_slice()) + ); + } + #[test] fn session_config_default_wire_flags_off_without_handlers() { let cfg = SessionConfig::default(); @@ -5720,7 +6095,7 @@ mod tests { #[test] fn session_config_builder_composes() { - use std::collections::HashMap; + use indexmap::IndexMap; let cfg = SessionConfig::default() .with_session_id(SessionId::from("sess-1")) @@ -5733,7 +6108,7 @@ mod tests { .with_tools([Tool::new("greet")]) .with_available_tools(["bash", "view"]) .with_excluded_tools(["dangerous"]) - .with_mcp_servers(HashMap::new()) + .with_mcp_servers(IndexMap::new()) .with_mcp_oauth_token_storage("persistent") .with_enable_config_discovery(true) .with_enable_on_demand_instruction_discovery(true) @@ -5794,7 +6169,7 @@ mod tests { #[test] fn resume_session_config_builder_composes() { - use std::collections::HashMap; + use indexmap::IndexMap; let cfg = ResumeSessionConfig::new(SessionId::from("sess-2")) .with_client_name("test-app") @@ -5804,7 +6179,7 @@ mod tests { .with_tools([Tool::new("greet")]) .with_available_tools(["bash", "view"]) .with_excluded_tools(["dangerous"]) - .with_mcp_servers(HashMap::new()) + .with_mcp_servers(IndexMap::new()) .with_mcp_oauth_token_storage("persistent") .with_enable_config_discovery(true) .with_enable_on_demand_instruction_discovery(false) @@ -5942,13 +6317,13 @@ mod tests { #[test] fn custom_agent_config_builder_composes() { - use std::collections::HashMap; + use indexmap::IndexMap; let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.") .with_display_name("Research Assistant") .with_description("Investigates technical questions.") .with_tools(["bash", "view"]) - .with_mcp_servers(HashMap::new()) + .with_mcp_servers(IndexMap::new()) .with_infer(true) .with_skills(["rust-coding-skill"]); @@ -5971,6 +6346,51 @@ mod tests { ); } + #[test] + fn mcp_servers_serialize_in_insertion_order() { + use indexmap::IndexMap; + + // Regression: `mcp_servers` was a `HashMap`, so the server keys (and + // thus the `session.create` payload) serialized in a per-process + // random order; `IndexMap` pins them to insertion order. The long + // sequence makes a `HashMap` regression reproduce this exact order by + // chance only 1/N!, avoiding a flaky false pass. + let order = [ + "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon", + "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet", + ]; + let mut servers = IndexMap::new(); + for name in order { + servers.insert( + name.to_string(), + McpServerConfig::Stdio(McpStdioServerConfig { + command: "run".to_string(), + ..Default::default() + }), + ); + } + + let (wire, _runtime) = SessionConfig::default() + .with_mcp_servers(servers) + .into_wire(None) + .expect("into_wire should succeed"); + let json = serde_json::to_string(&wire).expect("serialize wire"); + + let positions: Vec = order + .iter() + .map(|name| { + json.find(&format!("\"{name}\"")) + .unwrap_or_else(|| panic!("server {name} missing from wire JSON")) + }) + .collect(); + let mut ascending = positions.clone(); + ascending.sort_unstable(); + assert_eq!( + positions, ascending, + "mcp server keys must serialize in insertion order: {json}" + ); + } + #[test] fn infinite_session_config_builder_composes() { let cfg = InfiniteSessionConfig::new() diff --git a/rust/src/wire.rs b/rust/src/wire.rs index f73870fa53..43188bc274 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -13,9 +13,9 @@ //! configs hold trait-object handlers, the wire structs hold only the //! plain data the runtime needs. -use std::collections::HashMap; use std::path::PathBuf; +use indexmap::IndexMap; use serde::Serialize; use crate::canvas::CanvasDeclaration; @@ -24,10 +24,10 @@ use crate::generated::api_types::{ }; use crate::generated::session_events::ReasoningSummary; use crate::types::{ - CapiSessionOptions, CloudSessionOptions, CustomAgentConfig, DefaultAgentConfig, ExtensionInfo, - InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, MemoryConfiguration, - NamedProviderConfig, ProviderConfig, ProviderModelConfig, SessionId, SessionLimitsConfig, - SystemMessageConfig, Tool, + CanvasProviderIdentity, CapiSessionOptions, CloudSessionOptions, CustomAgentConfig, + DefaultAgentConfig, ExtensionInfo, InfiniteSessionConfig, LargeToolOutputConfig, + McpServerConfig, MemoryConfiguration, NamedProviderConfig, ProviderConfig, ProviderModelConfig, + SessionId, SessionLimitsConfig, SystemMessageConfig, Tool, ToolSearchConfig, }; /// Wire representation of a slash command (name + description only). The @@ -74,6 +74,8 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub extension_info: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub canvas_provider: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub available_tools: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub excluded_tools: Option>, @@ -83,7 +85,7 @@ pub(crate) struct SessionCreateWire { /// naturally (everything matching X except Y). pub tool_filter_precedence: &'static str, #[serde(skip_serializing_if = "Option::is_none")] - pub mcp_servers: Option>, + pub mcp_servers: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub mcp_oauth_token_storage: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -121,6 +123,8 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub large_output: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub tool_search: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub disabled_skills: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub custom_agents: Option>, @@ -169,6 +173,8 @@ pub(crate) struct SessionCreateWire { pub commands: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub exp_assignments: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_managed_settings: Option, } /// The exact JSON shape sent on the `session.resume` JSON-RPC request. @@ -205,6 +211,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub extension_info: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub canvas_provider: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub available_tools: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub excluded_tools: Option>, @@ -213,7 +221,7 @@ pub(crate) struct SessionResumeWire { /// SDK always sends `"excluded"`. See create-wire docs. pub tool_filter_precedence: &'static str, #[serde(skip_serializing_if = "Option::is_none")] - pub mcp_servers: Option>, + pub mcp_servers: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub mcp_oauth_token_storage: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -251,6 +259,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub large_output: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub tool_search: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub disabled_skills: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub custom_agents: Option>, @@ -302,4 +312,6 @@ pub(crate) struct SessionResumeWire { pub continue_pending_work: Option, #[serde(skip_serializing_if = "Option::is_none")] pub exp_assignments: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_managed_settings: Option, } diff --git a/rust/tests/cli_resolution_test.rs b/rust/tests/cli_resolution_test.rs index c3044a9e75..9e4927e676 100644 --- a/rust/tests/cli_resolution_test.rs +++ b/rust/tests/cli_resolution_test.rs @@ -196,21 +196,27 @@ async fn extract_dir_runtime_override_is_honored() { let _ = fake; } -/// Build-time version pin: `cli-version.txt` (when present) must be a -/// combined snapshot — a `version=X.Y.Z` line plus per-asset hash lines. +/// 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` — /// 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 pin = PathBuf::from(manifest_dir).join("cli-version.txt"); + 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.txt"); + 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('#') { @@ -222,9 +228,28 @@ fn pin_file_when_present_is_well_formed() { 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:?}" + ); + } else { + assert_eq!( + value.trim().len(), + 64, + "invalid SHA-256 hash for key {key:?}" + ); + assert!( + value.trim().bytes().all(|byte| byte.is_ascii_hexdigit()), + "invalid SHA-256 hash for key {key:?}" + ); + } + package_count += 1; } } - assert!(saw_version, "cli-version.txt missing `version=` line"); + assert!(saw_version, "{filename} missing `version=` line"); + assert_eq!(package_count, 6); } /// With `bundled-cli` on AND a supported target, `install_bundled_cli` @@ -246,6 +271,26 @@ 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() + ); + } } /// `install_bundled_cli` returns the same path the runtime resolver diff --git a/rust/tests/e2e.rs b/rust/tests/e2e.rs index 62412963b8..3a698abd18 100644 --- a/rust/tests/e2e.rs +++ b/rust/tests/e2e.rs @@ -37,6 +37,9 @@ mod github_telemetry; mod hooks; #[path = "e2e/hooks_extended.rs"] mod hooks_extended; +#[cfg(feature = "bundled-in-process")] +#[path = "e2e/inprocess.rs"] +mod inprocess; #[path = "e2e/mcp_and_agents.rs"] mod mcp_and_agents; #[path = "e2e/mcp_oauth.rs"] diff --git a/rust/tests/e2e/byok_bearer_token_provider.rs b/rust/tests/e2e/byok_bearer_token_provider.rs index fc3ef89d94..a7989d157f 100644 --- a/rust/tests/e2e/byok_bearer_token_provider.rs +++ b/rust/tests/e2e/byok_bearer_token_provider.rs @@ -142,6 +142,21 @@ async fn run_turn( #[tokio::test] async fn callback_token_is_applied_as_authorization_header() { + // The runtime's LLM inference provider slot is process-global and is never released + // when the registering connection disconnects (runtime `shared_api/llm_inference.rs`). + // Over the in-process transport all clients share this process's runtime, so once a + // BYOK provider is registered here and the client stops, the dangling registration + // routes every later model-inference request (list-models, tool-using turns, hooks, + // …) to the dead connection and hangs them. Registering a BYOK provider in-process + // therefore poisons the shared runtime for the rest of the suite. The BYOK bearer-token + // wiring is covered over stdio (a separate child process per test); the SDK-side + // request/response plumbing is transport-agnostic. + if super::support::skip_inprocess( + "registering a BYOK LLM inference provider is process-global in-process and is never \ + released on disconnect, poisoning later model-inference tests", + ) { + return; + } with_e2e_context_no_snapshot(|ctx| { Box::pin(async move { ctx.set_default_copilot_user(); @@ -189,6 +204,18 @@ async fn callback_token_is_applied_as_authorization_header() { #[tokio::test] async fn reacquires_a_fresh_token_for_each_request() { + // The runtime registers the LLM inference provider per connection and, by design, + // never releases the slot on disconnect (runtime `shared_api/llm_inference.rs`). Over + // the in-process transport every client shares this process's runtime, so a second + // provider-registering client is refused ("Another client is already the LLM + // inference provider"). The BYOK bearer-token behavior over the in-process transport + // is covered by `callback_token_is_applied_as_authorization_header`; this scenario's + // provider-dispatch logic is transport-agnostic and is covered over stdio. + if super::support::skip_inprocess( + "llmInference.setProvider is process-global in-process; a second provider client is refused", + ) { + return; + } with_e2e_context_no_snapshot(|ctx| { Box::pin(async move { ctx.set_default_copilot_user(); @@ -252,6 +279,16 @@ async fn reacquires_a_fresh_token_for_each_request() { #[tokio::test] async fn dispatches_token_acquisition_per_provider() { + // See `reacquires_a_fresh_token_for_each_request`: in-process, the process-global LLM + // inference provider registration is not released on disconnect, so this additional + // provider-registering client is refused. The BYOK transport path is covered in-process + // by `callback_token_is_applied_as_authorization_header`; the per-provider dispatch + // logic exercised here is transport-agnostic and covered over stdio. + if super::support::skip_inprocess( + "llmInference.setProvider is process-global in-process; a second provider client is refused", + ) { + return; + } with_e2e_context_no_snapshot(|ctx| { Box::pin(async move { ctx.set_default_copilot_user(); diff --git a/rust/tests/e2e/client.rs b/rust/tests/e2e/client.rs index 114e828ac9..6dd0f27acf 100644 --- a/rust/tests/e2e/client.rs +++ b/rust/tests/e2e/client.rs @@ -64,8 +64,7 @@ async fn should_get_authenticated_status() { Box::pin(async move { ctx.set_default_copilot_user(); let client = Client::start( - ctx.client_options() - .with_github_token(super::support::DEFAULT_TEST_TOKEN), + ctx.client_options_with_github_token(super::support::DEFAULT_TEST_TOKEN), ) .await .expect("start client"); @@ -85,8 +84,7 @@ async fn should_list_models_when_authenticated() { Box::pin(async move { ctx.set_default_copilot_user(); let client = Client::start( - ctx.client_options() - .with_github_token(super::support::DEFAULT_TEST_TOKEN), + ctx.client_options_with_github_token(super::support::DEFAULT_TEST_TOKEN), ) .await .expect("start client"); diff --git a/rust/tests/e2e/client_options.rs b/rust/tests/e2e/client_options.rs index dbf3c5c83d..85ef835025 100644 --- a/rust/tests/e2e/client_options.rs +++ b/rust/tests/e2e/client_options.rs @@ -6,7 +6,7 @@ use github_copilot_sdk::rpc::{OpenCanvasInstance, RemoteSessionMode}; use github_copilot_sdk::session_events::{ReasoningSummary, SessionLimitsConfig}; use github_copilot_sdk::{ CliProgram, Client, ClientOptions, ExtensionInfo, ProviderConfig, ResumeSessionConfig, - SessionConfig, SessionId, + SessionConfig, SessionId, Transport, }; use serde::Deserialize; use serde_json::{Value, json}; @@ -236,6 +236,7 @@ async fn should_forward_advanced_session_resume_options_to_the_cli() { canvas_id: "resume-canvas".to_string(), extension_id: "github-app/rust-e2e-extension".to_string(), extension_name: None, + icon: None, input: Some(json!({ "value": "from-resume" })), instance_id: "resume-instance".to_string(), status: None, @@ -347,6 +348,7 @@ impl FakeCli { ]) .with_github_token(token) .with_use_logged_in_user(false) + .with_transport(Transport::Stdio) } fn path(&self, name: &str) -> PathBuf { diff --git a/rust/tests/e2e/copilot_request_handler.rs b/rust/tests/e2e/copilot_request_handler.rs index 6ca99393bf..46b4e510cd 100644 --- a/rust/tests/e2e/copilot_request_handler.rs +++ b/rust/tests/e2e/copilot_request_handler.rs @@ -502,6 +502,9 @@ async fn start_ws_upstream(counters: HandlerCounters) -> String { #[tokio::test] async fn services_http_and_websocket_via_handler() { + if super::support::skip_inprocess("LLM inference providers are process-global in-process") { + return; + } with_e2e_context_no_snapshot(|ctx| { Box::pin(async move { ctx.set_default_copilot_user(); @@ -572,16 +575,25 @@ async fn services_http_and_websocket_via_handler() { #[derive(Default)] struct RecordingHandler { - records: std::sync::Mutex)>>, + records: std::sync::Mutex>, +} + +#[derive(Clone)] +struct InterceptedRequest { + url: String, + session_id: Option, + agent_id: Option, + parent_agent_id: Option, + interaction_type: Option, } impl RecordingHandler { - fn inference_records(&self) -> Vec<(String, Option)> { + fn inference_records(&self) -> Vec { self.records .lock() .unwrap() .iter() - .filter(|(url, _)| is_inference_url(url)) + .filter(|record| is_inference_url(&record.url)) .cloned() .collect() } @@ -594,10 +606,13 @@ impl CopilotRequestHandler for RecordingHandler { request: CopilotHttpRequest, ctx: &CopilotRequestContext, ) -> Result { - self.records - .lock() - .unwrap() - .push((request.url.clone(), ctx.session_id.clone())); + self.records.lock().unwrap().push(InterceptedRequest { + url: request.url.clone(), + session_id: ctx.session_id.clone(), + agent_id: ctx.agent_id.clone(), + parent_agent_id: ctx.parent_agent_id.clone(), + interaction_type: ctx.interaction_type.clone(), + }); if is_inference_url(&request.url) { Ok(synth_inference_response( &request.url, @@ -612,6 +627,9 @@ impl CopilotRequestHandler for RecordingHandler { #[tokio::test] async fn threads_session_id_into_inference() { + if super::support::skip_inprocess("LLM inference providers are process-global in-process") { + return; + } with_e2e_context_no_snapshot(|ctx| { Box::pin(async move { ctx.set_default_copilot_user(); @@ -632,12 +650,13 @@ async fn threads_session_id_into_inference() { !inference.is_empty(), "expected at least one intercepted inference request" ); - for (_, session_id) in &inference { + for record in &inference { assert_eq!( - session_id.as_deref(), + record.session_id.as_deref(), Some(capi_session_id.as_str()), "CAPI inference request must carry the session id" ); + assert_agent_metadata(record); } assert!( assistant_text(&result).contains("OK from the synthetic"), @@ -671,12 +690,13 @@ async fn threads_session_id_into_inference() { inference.len() > before, "expected at least one intercepted BYOK inference request" ); - for (_, session_id) in &inference[before..] { + for record in &inference[before..] { assert_eq!( - session_id.as_deref(), + record.session_id.as_deref(), Some(byok_session_id.as_str()), "BYOK inference request must carry the session id" ); + assert_agent_metadata(record); } assert_ne!( byok_session_id, capi_session_id, @@ -694,6 +714,26 @@ async fn threads_session_id_into_inference() { .await; } +fn assert_agent_metadata(record: &InterceptedRequest) { + assert!( + record.agent_id.as_deref().is_some_and(|id| !id.is_empty()), + "inference request must carry an agent id" + ); + if let Some(parent_agent_id) = record.parent_agent_id.as_deref() { + assert!( + !parent_agent_id.is_empty(), + "parent agent id must be non-empty when present" + ); + } + assert!( + record + .interaction_type + .as_deref() + .is_some_and(|kind| !kind.is_empty()), + "inference request must carry an interaction type" + ); +} + // --------------------------------------------------------------------------- // Scenario 3a: errors — a handler that returns `Err` on an inference request // surfaces a transport error rather than hanging the turn. @@ -723,6 +763,9 @@ impl CopilotRequestHandler for ThrowingHandler { #[tokio::test] async fn surfaces_handler_errors() { + if super::support::skip_inprocess("LLM inference providers are process-global in-process") { + return; + } with_e2e_context_no_snapshot(|ctx| { Box::pin(async move { ctx.set_default_copilot_user(); @@ -789,6 +832,9 @@ impl CopilotRequestHandler for CancellingHandler { #[tokio::test] async fn observes_runtime_driven_cancel() { + if super::support::skip_inprocess("LLM inference providers are process-global in-process") { + return; + } with_e2e_context_no_snapshot(|ctx| { Box::pin(async move { ctx.set_default_copilot_user(); diff --git a/rust/tests/e2e/hooks_extended.rs b/rust/tests/e2e/hooks_extended.rs index 259d462d56..ab93a0c3cd 100644 --- a/rust/tests/e2e/hooks_extended.rs +++ b/rust/tests/e2e/hooks_extended.rs @@ -36,7 +36,7 @@ async fn should_invoke_onsessionstart_hook_on_new_session() { session.send_and_wait("Say hi").await.expect("send"); let input = recv_with_timeout(&mut rx, "sessionStart hook").await; assert_eq!(input.source, "new"); - assert!(input.timestamp > 0); + assert!(input.timestamp > 0.0); assert!(!input.working_directory.as_os_str().is_empty()); session.disconnect().await.expect("disconnect session"); @@ -68,7 +68,7 @@ async fn should_invoke_onuserpromptsubmitted_hook_when_sending_a_message() { session.send_and_wait("Say hello").await.expect("send"); let input = recv_with_timeout(&mut rx, "userPromptSubmitted hook").await; assert!(input.prompt.contains("Say hello")); - assert!(input.timestamp > 0); + assert!(input.timestamp > 0.0); assert!(!input.working_directory.as_os_str().is_empty()); session.disconnect().await.expect("disconnect session"); @@ -100,7 +100,7 @@ async fn should_invoke_onsessionend_hook_when_session_is_disconnected() { session.send_and_wait("Say hi").await.expect("send"); session.disconnect().await.expect("disconnect session"); let input = recv_with_timeout(&mut rx, "sessionEnd hook").await; - assert!(input.timestamp > 0); + assert!(input.timestamp > 0.0); assert!(!input.working_directory.as_os_str().is_empty()); client.stop().await.expect("stop client"); @@ -237,7 +237,7 @@ async fn should_invoke_sessionend_hook() { session.send_and_wait("Say bye").await.expect("send"); session.disconnect().await.expect("disconnect session"); let input = recv_with_timeout(&mut rx, "sessionEnd hook").await; - assert!(input.timestamp > 0); + assert!(input.timestamp > 0.0); client.stop().await.expect("stop client"); }) @@ -412,7 +412,7 @@ async fn should_invoke_posttoolusefailure_hook_for_failed_tool_result() { .as_str() .is_some_and(|path| path.contains("missing.txt")) ); - assert!(input.timestamp > 0); + assert!(input.timestamp > 0.0); assert!(!input.working_directory.as_os_str().is_empty()); assert!( assistant_message_content(&answer).contains("HOOK_FAILURE_GUIDANCE_APPLIED") diff --git a/rust/tests/e2e/inprocess.rs b/rust/tests/e2e/inprocess.rs new file mode 100644 index 0000000000..0c183a27df --- /dev/null +++ b/rust/tests/e2e/inprocess.rs @@ -0,0 +1,25 @@ +use super::support::with_e2e_context; + +/// Starts an in-process client, performs a round-trip, and stops cleanly. +/// Fails hard if the in-process runtime library cannot be loaded. +#[tokio::test] +async fn should_start_ping_and_stop_inprocess_client() { + with_e2e_context("client", "should_start_ping_and_stop_stdio_client", |ctx| { + Box::pin(async move { + let client = ctx.start_inprocess_client().await; + + let response = client + .ping(Some("hello from rust in-process")) + .await + .expect("ping over in-process FFI transport"); + assert_eq!(response.message, "pong: hello from rust in-process"); + assert!(!response.timestamp.is_empty()); + + let status = client.get_status().await.expect("get status"); + assert!(status.protocol_version > 0); + + client.stop().await.expect("stop in-process client"); + }) + }) + .await; +} diff --git a/rust/tests/e2e/mcp_oauth.rs b/rust/tests/e2e/mcp_oauth.rs index f330df1155..5a11ef4b7a 100644 --- a/rust/tests/e2e/mcp_oauth.rs +++ b/rust/tests/e2e/mcp_oauth.rs @@ -8,7 +8,7 @@ use github_copilot_sdk::handler::{McpAuthHandler, McpAuthRequest, McpAuthResult} use github_copilot_sdk::rpc::{McpAppsCallToolRequest, McpListToolsRequest}; use github_copilot_sdk::session::Session; use github_copilot_sdk::session_events::{McpOauthRequestReason, McpServerStatus}; -use github_copilot_sdk::{McpHttpServerConfig, McpServerConfig, RequestId, SessionId}; +use github_copilot_sdk::{IndexMap, McpHttpServerConfig, McpServerConfig, RequestId, SessionId}; use parking_lot::Mutex; use serde::Deserialize; use serde_json::Value; @@ -40,7 +40,7 @@ async fn should_satisfy_mcp_oauth_using_host_provided_token() { .create_session( ctx.approve_all_session_config() .with_mcp_auth_handler(handler.clone()) - .with_mcp_servers(HashMap::from([( + .with_mcp_servers(IndexMap::from([( server_name.to_string(), McpServerConfig::Http(McpHttpServerConfig { tools: Some(vec!["*".to_string()]), @@ -129,7 +129,7 @@ async fn should_request_replacement_tokens_across_mcp_oauth_lifecycle() { ctx.approve_all_session_config() .with_enable_mcp_apps(true) .with_mcp_auth_handler(handler.clone()) - .with_mcp_servers(HashMap::from([( + .with_mcp_servers(IndexMap::from([( server_name.to_string(), McpServerConfig::Http(McpHttpServerConfig { tools: Some(vec!["*".to_string()]), @@ -194,7 +194,7 @@ async fn should_cancel_pending_mcp_oauth_request() { .create_session( ctx.approve_all_session_config() .with_mcp_auth_handler(handler.clone()) - .with_mcp_servers(HashMap::from([( + .with_mcp_servers(IndexMap::from([( server_name.to_string(), McpServerConfig::Http(McpHttpServerConfig { tools: Some(vec!["*".to_string()]), @@ -250,7 +250,7 @@ async fn should_resolve_pending_mcp_oauth_request_through_rpc() { ctx.approve_all_session_config() .with_enable_mcp_apps(true) .with_mcp_auth_handler(handler) - .with_mcp_servers(HashMap::from([( + .with_mcp_servers(IndexMap::from([( server_name.to_string(), McpServerConfig::Http(McpHttpServerConfig { tools: Some(vec!["*".to_string()]), diff --git a/rust/tests/e2e/mode_handlers.rs b/rust/tests/e2e/mode_handlers.rs index fbaaf5158d..b4089ca28a 100644 --- a/rust/tests/e2e/mode_handlers.rs +++ b/rust/tests/e2e/mode_handlers.rs @@ -134,7 +134,7 @@ async fn should_invoke_exit_plan_mode_handler_when_model_uses_tool() { assert_eq!(request.summary, PLAN_SUMMARY); assert_eq!( request.actions, - ["interactive", "autopilot", "exit_only"].map(str::to_string) + ["autopilot", "interactive", "exit_only"].map(str::to_string) ); assert_eq!(request.recommended_action, "interactive"); @@ -146,8 +146,8 @@ async fn should_invoke_exit_plan_mode_handler_when_model_uses_tool() { assert_eq!( requested_data.actions, [ - ExitPlanModeAction::Interactive, ExitPlanModeAction::Autopilot, + ExitPlanModeAction::Interactive, ExitPlanModeAction::ExitOnly, ] ); diff --git a/rust/tests/e2e/per_session_auth.rs b/rust/tests/e2e/per_session_auth.rs index b2fd11e4d4..efb005b590 100644 --- a/rust/tests/e2e/per_session_auth.rs +++ b/rust/tests/e2e/per_session_auth.rs @@ -7,6 +7,9 @@ use super::support::with_e2e_context; #[tokio::test] async fn session_uses_client_token_when_no_session_token_is_supplied() { + if super::support::skip_inprocess("client-level GitHub tokens are not supported in-process") { + return; + } with_e2e_context( "per-session-auth", "session_uses_client_token_when_no_session_token_is_supplied", @@ -47,6 +50,9 @@ async fn session_uses_client_token_when_no_session_token_is_supplied() { #[tokio::test] async fn session_token_overrides_client_token() { + if super::support::skip_inprocess("client-level GitHub tokens are not supported in-process") { + return; + } with_e2e_context( "per-session-auth", "session_token_overrides_client_token", @@ -93,7 +99,11 @@ async fn session_auth_status_is_unauthenticated_without_token() { "session_auth_status_is_unauthenticated_without_token", |ctx| { Box::pin(async move { - let client = ctx.start_client().await; + let client = github_copilot_sdk::Client::start( + ctx.client_options().with_use_logged_in_user(false), + ) + .await + .expect("start client"); let session = client .create_session( SessionConfig::default() diff --git a/rust/tests/e2e/pre_mcp_tool_call_hook.rs b/rust/tests/e2e/pre_mcp_tool_call_hook.rs index 973672f709..fd05796fcd 100644 --- a/rust/tests/e2e/pre_mcp_tool_call_hook.rs +++ b/rust/tests/e2e/pre_mcp_tool_call_hook.rs @@ -1,23 +1,22 @@ -use std::collections::HashMap; use std::sync::Arc; use async_trait::async_trait; use github_copilot_sdk::hooks::{ HookContext, PreMcpToolCallInput, PreMcpToolCallOutput, SessionHooks, }; -use github_copilot_sdk::{McpServerConfig, McpStdioServerConfig}; +use github_copilot_sdk::{IndexMap, McpServerConfig, McpStdioServerConfig}; use serde_json::{Value, json}; use tokio::sync::mpsc; use super::support::{assistant_message_content, recv_with_timeout, with_e2e_context}; -fn meta_echo_mcp_servers(repo_root: &std::path::Path) -> HashMap { +fn meta_echo_mcp_servers(repo_root: &std::path::Path) -> IndexMap { let harness_dir = repo_root.join("test").join("harness"); let server_path = harness_dir .join("test-mcp-meta-echo-server.mjs") .to_string_lossy() .to_string(); - HashMap::from([( + IndexMap::from([( "meta-echo".to_string(), McpServerConfig::Stdio(McpStdioServerConfig { tools: Some(vec!["*".to_string()]), @@ -127,7 +126,7 @@ async fn should_set_meta_via_premcptoolcall_hook() { assert_eq!(input.server_name, "meta-echo"); assert_eq!(input.tool_name, "echo_meta"); assert!(!input.working_directory.as_os_str().is_empty()); - assert!(input.timestamp > 0); + assert!(input.timestamp > 0.0); session.disconnect().await.expect("disconnect session"); client.stop().await.expect("stop client"); diff --git a/rust/tests/e2e/provider_endpoint.rs b/rust/tests/e2e/provider_endpoint.rs index df6d5941dd..3953aad669 100644 --- a/rust/tests/e2e/provider_endpoint.rs +++ b/rust/tests/e2e/provider_endpoint.rs @@ -15,6 +15,7 @@ fn opt_in_env() -> (OsString, OsString) { } #[tokio::test] +#[allow(deprecated)] async fn byok_provider_endpoint_returns_configured_endpoint() { with_e2e_context( "provider-endpoint", @@ -22,7 +23,9 @@ async fn byok_provider_endpoint_returns_configured_endpoint() { |ctx| { Box::pin(async move { let mut options = ctx.client_options(); - options.env.push(opt_in_env()); + if !super::support::is_inprocess_default() { + options.env.push(opt_in_env()); + } let client = github_copilot_sdk::Client::start(options) .await .expect("start client"); @@ -86,6 +89,7 @@ async fn byok_provider_endpoint_returns_configured_endpoint() { } #[tokio::test] +#[allow(deprecated)] async fn capi_provider_endpoint_returns_resolved_credentials() { with_e2e_context( "provider-endpoint", @@ -93,8 +97,10 @@ async fn capi_provider_endpoint_returns_resolved_credentials() { |ctx| { Box::pin(async move { ctx.set_default_copilot_user(); - let mut options = ctx.client_options().with_github_token(DEFAULT_TEST_TOKEN); - options.env.push(opt_in_env()); + let mut options = ctx.client_options_with_github_token(DEFAULT_TEST_TOKEN); + if !super::support::is_inprocess_default() { + options.env.push(opt_in_env()); + } let client = github_copilot_sdk::Client::start(options) .await .expect("start client"); diff --git a/rust/tests/e2e/rpc_mcp_and_skills.rs b/rust/tests/e2e/rpc_mcp_and_skills.rs index 3f6cc05025..eb8368ebcd 100644 --- a/rust/tests/e2e/rpc_mcp_and_skills.rs +++ b/rust/tests/e2e/rpc_mcp_and_skills.rs @@ -3,16 +3,16 @@ use std::path::Path; use github_copilot_sdk::rpc::{ ExtensionsDisableRequest, ExtensionsEnableRequest, McpAppsCallToolRequest, - McpAppsDiagnoseRequest, McpAppsListToolsRequest, McpAppsReadResourceRequest, - McpAppsSetHostContextDetails, McpAppsSetHostContextDetailsAvailableDisplayMode, - McpAppsSetHostContextDetailsDisplayMode, McpAppsSetHostContextDetailsPlatform, - McpAppsSetHostContextDetailsTheme, McpAppsSetHostContextRequest, - McpCancelSamplingExecutionParams, McpDisableRequest, McpEnableRequest, - McpExecuteSamplingParams, McpExecuteSamplingRequest, McpOauthLoginRequest, - McpSamplingExecutionAction, McpSetEnvValueModeDetails, McpSetEnvValueModeParams, + McpAppsDiagnoseRequest, McpAppsListToolsRequest, McpAppsSetHostContextDetails, + McpAppsSetHostContextDetailsAvailableDisplayMode, McpAppsSetHostContextDetailsDisplayMode, + McpAppsSetHostContextDetailsPlatform, McpAppsSetHostContextDetailsTheme, + McpAppsSetHostContextRequest, McpCancelSamplingExecutionParams, McpDisableRequest, + McpEnableRequest, McpExecuteSamplingParams, McpExecuteSamplingRequest, McpOauthLoginRequest, + McpResourcesReadRequest, McpSamplingExecutionAction, McpSetEnvValueModeDetails, + McpSetEnvValueModeParams, PermissionsAllowAllMode, PermissionsSetAllowAllRequest, SkillsDisableRequest, SkillsEnableRequest, }; -use github_copilot_sdk::{McpServerConfig, McpStdioServerConfig}; +use github_copilot_sdk::{IndexMap, McpServerConfig, McpStdioServerConfig}; use super::support::with_e2e_context; @@ -340,14 +340,22 @@ async fn should_list_extensions() { with_e2e_context("rpc_mcp_and_skills", "should_list_extensions", |ctx| { Box::pin(async move { ctx.set_default_copilot_user(); - let client = - github_copilot_sdk::Client::start(ctx.client_options().with_extra_args(["--yolo"])) - .await - .expect("start yolo client"); + let client = ctx.start_client().await; let session = client .create_session(ctx.approve_all_session_config()) .await .expect("create session"); + session + .rpc() + .permissions() + .set_allow_all(PermissionsSetAllowAllRequest { + enabled: None, + mode: Some(PermissionsAllowAllMode::On), + model: None, + source: None, + }) + .await + .expect("enable allow-all"); let result = session .rpc() @@ -510,8 +518,8 @@ async fn should_report_error_when_mcp_app_resource_is_not_available() { let err = session .rpc() .mcp() - .apps() - .read_resource(McpAppsReadResourceRequest { + .resources() + .read(McpResourcesReadRequest { server_name: "missing-app-server".to_string(), uri: "ui://missing/resource.html".to_string(), }) @@ -678,15 +686,22 @@ async fn should_report_error_when_extensions_are_not_available() { |ctx| { Box::pin(async move { ctx.set_default_copilot_user(); - let client = github_copilot_sdk::Client::start( - ctx.client_options().with_extra_args(["--yolo"]), - ) - .await - .expect("start client"); + let client = ctx.start_client().await; let session = client .create_session(ctx.approve_all_session_config()) .await .expect("create session"); + session + .rpc() + .permissions() + .set_allow_all(PermissionsSetAllowAllRequest { + enabled: None, + mode: Some(PermissionsAllowAllMode::On), + model: None, + source: None, + }) + .await + .expect("enable allow-all"); expect_err_contains( session.rpc().extensions().enable(ExtensionsEnableRequest { @@ -761,14 +776,14 @@ fn assert_skill( skill } -fn test_mcp_servers(repo_root: &Path, server_name: &str) -> HashMap { +fn test_mcp_servers(repo_root: &Path, server_name: &str) -> IndexMap { let harness_dir = repo_root.join("test").join("harness"); let server_path = harness_dir .join("test-mcp-server.mjs") .to_string_lossy() .to_string(); - HashMap::from([( + IndexMap::from([( server_name.to_string(), McpServerConfig::Stdio(McpStdioServerConfig { tools: Some(vec!["*".to_string()]), diff --git a/rust/tests/e2e/rpc_mcp_lifecycle.rs b/rust/tests/e2e/rpc_mcp_lifecycle.rs index fa2b15d866..028b5a527f 100644 --- a/rust/tests/e2e/rpc_mcp_lifecycle.rs +++ b/rust/tests/e2e/rpc_mcp_lifecycle.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::path::Path; use github_copilot_sdk::rpc::{ @@ -7,7 +6,7 @@ use github_copilot_sdk::rpc::{ }; use github_copilot_sdk::session::Session; use github_copilot_sdk::session_events::McpServerStatus; -use github_copilot_sdk::{Error, McpServerConfig, McpStdioServerConfig}; +use github_copilot_sdk::{Error, IndexMap, McpServerConfig, McpStdioServerConfig}; use serde::de::DeserializeOwned; use serde_json::{Value, json}; @@ -330,8 +329,8 @@ async fn should_configure_github_mcp_server() { fn create_test_mcp_servers( repo_root: &Path, server_name: &str, -) -> HashMap { - HashMap::from([(server_name.to_string(), test_mcp_server_config(repo_root))]) +) -> IndexMap { + IndexMap::from([(server_name.to_string(), test_mcp_server_config(repo_root))]) } fn test_mcp_server_config(repo_root: &Path) -> McpServerConfig { diff --git a/rust/tests/e2e/rpc_server.rs b/rust/tests/e2e/rpc_server.rs index 665041f49d..d0beab2452 100644 --- a/rust/tests/e2e/rpc_server.rs +++ b/rust/tests/e2e/rpc_server.rs @@ -54,7 +54,7 @@ async fn should_call_rpc_models_list_with_typed_result() { Box::pin(async move { let token = "rpc-models-token"; ctx.set_copilot_user_by_token_with_login(token, "rpc-user"); - let client = Client::start(ctx.client_options().with_github_token(token)) + let client = Client::start(ctx.client_options_with_github_token(token)) .await .expect("start client"); @@ -95,7 +95,7 @@ async fn should_call_rpc_account_get_quota_when_authenticated() { } })), ); - let client = Client::start(ctx.client_options().with_github_token(token)) + let client = Client::start(ctx.client_options_with_github_token(token)) .await .expect("start client"); diff --git a/rust/tests/e2e/rpc_server_plugins.rs b/rust/tests/e2e/rpc_server_plugins.rs index dc841b0af6..054ffa3599 100644 --- a/rust/tests/e2e/rpc_server_plugins.rs +++ b/rust/tests/e2e/rpc_server_plugins.rs @@ -31,6 +31,7 @@ async fn should_install_and_list_plugin_from_local_marketplace() { .marketplaces() .add(PluginsMarketplacesAddRequest { source: marketplace.source(), + working_directory: None, }) .await .expect("add marketplace"); @@ -79,6 +80,7 @@ async fn should_enable_and_disable_marketplace_plugin() { .marketplaces() .add(PluginsMarketplacesAddRequest { source: marketplace.source(), + working_directory: None, }) .await .expect("add marketplace"); @@ -148,6 +150,7 @@ async fn should_update_single_marketplace_plugin() { .marketplaces() .add(PluginsMarketplacesAddRequest { source: marketplace.source(), + working_directory: None, }) .await .expect("add marketplace"); @@ -196,6 +199,7 @@ async fn should_update_all_installed_plugins() { .marketplaces() .add(PluginsMarketplacesAddRequest { source: marketplace.source(), + working_directory: None, }) .await .expect("add marketplace"); @@ -326,6 +330,7 @@ async fn should_list_browse_refresh_and_remove_local_marketplace() { .marketplaces() .add(PluginsMarketplacesAddRequest { source: marketplace.source(), + working_directory: None, }) .await .expect("add marketplace"); diff --git a/rust/tests/e2e/rpc_session_state_extras.rs b/rust/tests/e2e/rpc_session_state_extras.rs index 2e7fbc44ad..3bd6c99bee 100644 --- a/rust/tests/e2e/rpc_session_state_extras.rs +++ b/rust/tests/e2e/rpc_session_state_extras.rs @@ -22,7 +22,7 @@ async fn should_list_models_for_session() { Box::pin(async move { let token = "rpc-session-model-list-token"; ctx.set_copilot_user_by_token_with_login(token, "rpc-session-extras-user"); - let client = Client::start(ctx.client_options().with_github_token(token)) + let client = Client::start(ctx.client_options_with_github_token(token)) .await .expect("start authenticated client"); let session = client @@ -319,6 +319,7 @@ async fn should_add_byok_provider_and_model_at_runtime() { model_id: selection_id.to_string(), reasoning_effort: None, reasoning_summary: None, + verbosity: None, }) .await .expect("switch to added model"); diff --git a/rust/tests/e2e/rpc_workspace_checkpoints.rs b/rust/tests/e2e/rpc_workspace_checkpoints.rs index 2c185a535a..0a8bf56152 100644 --- a/rust/tests/e2e/rpc_workspace_checkpoints.rs +++ b/rust/tests/e2e/rpc_workspace_checkpoints.rs @@ -40,6 +40,12 @@ async fn should_list_no_checkpoints_for_fresh_session() { #[tokio::test] async fn should_return_null_or_empty_content_for_unknown_checkpoint() { + // In-process, session.workspaces.readCheckpoint is answered by the native runtime, + // which decodes the checkpoint number as a u32 and rejects the i64::MAX sentinel this + // test uses. Covered by the default (stdio) transport. See issue #1934. + if super::support::skip_inprocess("readCheckpoint decodes the id as u32 in-process") { + return; + } with_e2e_context( "rpc_workspace_checkpoints", "should_return_null_or_empty_content_for_unknown_checkpoint", diff --git a/rust/tests/e2e/session_config.rs b/rust/tests/e2e/session_config.rs index 0a7a5eb11b..dd498e3761 100644 --- a/rust/tests/e2e/session_config.rs +++ b/rust/tests/e2e/session_config.rs @@ -515,6 +515,9 @@ fn assert_anthropic_document_citations_enabled(request_body: &[u8]) { #[tokio::test] async fn should_enable_citations_for_anthropic_file_attachments_on_create() { + if super::support::skip_inprocess("LLM inference providers are process-global in-process") { + return; + } with_e2e_context_no_snapshot(|ctx| { Box::pin(async move { ctx.set_default_copilot_user(); diff --git a/rust/tests/e2e/subagent_hooks.rs b/rust/tests/e2e/subagent_hooks.rs index 99529c433b..fe94c36779 100644 --- a/rust/tests/e2e/subagent_hooks.rs +++ b/rust/tests/e2e/subagent_hooks.rs @@ -5,12 +5,19 @@ use github_copilot_sdk::hooks::{ HookContext, PostToolUseInput, PostToolUseOutput, PreToolUseInput, PreToolUseOutput, SessionHooks, }; +use github_copilot_sdk::{ + CopilotHttpRequest, CopilotHttpResponse, CopilotRequestContext, CopilotRequestError, + CopilotRequestHandler, forward_http, +}; use parking_lot::Mutex; use super::support::with_e2e_context; #[tokio::test] async fn should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls() { + if super::support::skip_inprocess("LLM inference providers are process-global in-process") { + return; + } with_e2e_context( "subagent_hooks", "should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls", @@ -24,16 +31,14 @@ async fn should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls .expect("write test file"); let hook_log = Arc::new(Mutex::new(Vec::::new())); + let request_log = Arc::new(RecordingRequestHandler::default()); - let mut opts = ctx.client_options(); - opts.env.push(( - "COPILOT_EXP_COPILOT_CLI_SESSION_BASED_SUBAGENTS".into(), - "true".into(), - )); - - let client = github_copilot_sdk::Client::start(opts) - .await - .expect("start client"); + let client = ctx + .start_llm_client( + Arc::clone(&request_log), + &[("COPILOT_EXP_COPILOT_CLI_SESSION_BASED_SUBAGENTS", "true")], + ) + .await; let session = client .create_session(ctx.approve_all_session_config().with_hooks(Arc::new( @@ -88,6 +93,7 @@ async fn should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls task_pre.unwrap().session_id, "Sub-agent tool hooks should have a different sessionId than parent tool hooks" ); + assert_subagent_request_metadata(&request_log.inference_records()); session.disconnect().await.expect("disconnect session"); client.stop().await.expect("stop client"); @@ -104,6 +110,90 @@ struct HookEntry { session_id: String, } +#[derive(Clone, Debug)] +struct RequestEntry { + url: String, + agent_id: Option, + parent_agent_id: Option, + interaction_type: Option, +} + +#[derive(Default)] +struct RecordingRequestHandler { + log: Mutex>, +} + +impl RecordingRequestHandler { + fn inference_records(&self) -> Vec { + self.log + .lock() + .iter() + .filter(|entry| is_inference_url(&entry.url)) + .cloned() + .collect() + } +} + +#[async_trait] +impl CopilotRequestHandler for RecordingRequestHandler { + async fn send_request( + &self, + request: CopilotHttpRequest, + ctx: &CopilotRequestContext, + ) -> Result { + self.log.lock().push(RequestEntry { + url: request.url.clone(), + agent_id: ctx.agent_id.clone(), + parent_agent_id: ctx.parent_agent_id.clone(), + interaction_type: ctx.interaction_type.clone(), + }); + forward_http(request).await + } +} + +fn is_inference_url(url: &str) -> bool { + let url = url.to_lowercase(); + url.ends_with("/chat/completions") + || url.ends_with("/responses") + || url.ends_with("/v1/messages") + || url.ends_with("/messages") +} + +fn assert_subagent_request_metadata(records: &[RequestEntry]) { + assert!( + !records.is_empty(), + "request handler should observe inference requests" + ); + let subagent_request = records + .iter() + .find(|entry| { + entry + .parent_agent_id + .as_deref() + .is_some_and(|id| !id.is_empty()) + }) + .expect("sub-agent inference request should carry a parentAgentId"); + assert!( + subagent_request + .agent_id + .as_deref() + .is_some_and(|id| !id.is_empty()), + "sub-agent inference request should carry an agentId" + ); + assert!( + subagent_request + .interaction_type + .as_deref() + .is_some_and(|kind| !kind.is_empty()), + "sub-agent inference request should carry an interactionType" + ); + assert_ne!( + subagent_request.parent_agent_id.as_deref(), + subagent_request.agent_id.as_deref(), + "sub-agent inference request should have distinct parent and child agent ids" + ); +} + struct RecordingHooks { log: Arc>>, } diff --git a/rust/tests/e2e/support.rs b/rust/tests/e2e/support.rs index 7e47d8fbae..6ad609f58e 100644 --- a/rust/tests/e2e/support.rs +++ b/rust/tests/e2e/support.rs @@ -1,4 +1,4 @@ -use std::ffi::OsString; +use std::ffi::{OsStr, OsString}; use std::future::Future; use std::io::{BufRead, BufReader, Read, Write}; use std::net::TcpStream; @@ -36,6 +36,13 @@ where .await .unwrap_or_else(|err| panic!("create E2E context: {err}")); + // In-process hosting: the runtime loads into this test process and its worker + // inherits the ambient environment (per-client env is not honored in-process, see + // https://github.com/github/copilot-sdk/issues/1934), so mirror this context's env + // onto the process for the duration of the test and restore on drop. Safe because + // E2E_CONCURRENCY is 1 in-process, serializing the whole critical section. + let _env_guard = InProcessEnvGuard::activate(&ctx); + let timed_out = tokio::time::timeout(default_test_timeout(), test(&mut ctx)) .await .is_err(); @@ -65,6 +72,10 @@ where .await .unwrap_or_else(|err| panic!("create E2E context: {err}")); + // See `with_e2e_context` for why the in-process transport mirrors env onto the + // process (restored on drop). + let _env_guard = InProcessEnvGuard::activate(&ctx); + let timed_out = tokio::time::timeout(default_test_timeout(), test(&mut ctx)) .await .is_err(); @@ -104,6 +115,7 @@ impl E2eContext { proxy: Some(proxy), }; ctx.configure(category, snapshot_name)?; + ctx.set_default_copilot_user(); Ok(ctx) } @@ -133,6 +145,7 @@ impl E2eContext { .map_err(|err| { std::io::Error::other(format!("configure proxy without snapshot failed: {err}")) })?; + ctx.set_default_copilot_user(); Ok(ctx) } @@ -164,12 +177,29 @@ impl E2eContext { self.client_options().with_transport(transport) } + pub fn client_options_with_github_token(&self, token: &str) -> ClientOptions { + self.client_options().with_github_token(token) + } + pub async fn start_client(&self) -> Client { Client::start(self.client_options()) .await .expect("start E2E client") } + /// Start a client that hosts the runtime in-process over FFI + /// ([`Transport::InProcess`]). Unlike the stdio harness, the CLI + /// entrypoint is passed as the program directly (the FFI host builds the + /// `node --embedded-host` argv itself and loads the sibling + /// runtime cdylib), so a `.js` entrypoint is not split into node + + /// prefix_args here. + pub async fn start_inprocess_client(&self) -> Client { + let options = ClientOptions::new().with_transport(Transport::InProcess); + Client::start(options) + .await + .expect("start in-process FFI E2E client") + } + /// Start a client wired to a Copilot request handler, appending `extra_env` /// to the spawned runtime's environment (used to flip the WebSocket ExP /// flag for the WS transport tests). @@ -302,11 +332,13 @@ impl E2eContext { ), ("COPILOT_MCP_APPS".into(), "true".into()), ("MCP_APPS".into(), "true".into()), + ("GH_TOKEN".into(), DEFAULT_TEST_TOKEN.into()), + ("GITHUB_TOKEN".into(), DEFAULT_TEST_TOKEN.into()), + ("GH_ENTERPRISE_TOKEN".into(), "".into()), + ("GITHUB_ENTERPRISE_TOKEN".into(), "".into()), + ("COPILOT_HMAC_KEY".into(), "".into()), + ("CAPI_HMAC_KEY".into(), "".into()), ]); - if std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true") { - env.push(("GH_TOKEN".into(), "fake-token-for-e2e-tests".into())); - env.push(("GITHUB_TOKEN".into(), "fake-token-for-e2e-tests".into())); - } env } @@ -528,6 +560,12 @@ fn default_test_timeout() -> Duration { } fn e2e_concurrency() -> usize { + // The in-process transport mirrors per-test environment onto the shared process + // environment (see `InProcessEnvGuard`), which is only coherent when one test runs + // at a time. Force serial execution in-process; otherwise honor RUST_E2E_CONCURRENCY. + if is_inprocess_default() { + return 1; + } std::env::var("RUST_E2E_CONCURRENCY") .ok() .and_then(|value| value.parse::().ok()) @@ -535,6 +573,104 @@ fn e2e_concurrency() -> usize { .unwrap_or(4) } +/// True when the E2E suite runs over the in-process (FFI) transport, i.e. the SDK +/// resolves `COPILOT_SDK_DEFAULT_CONNECTION=inprocess` to [`Transport::InProcess`]. +pub fn is_inprocess_default() -> bool { + std::env::var("COPILOT_SDK_DEFAULT_CONNECTION") + .map(|value| value.eq_ignore_ascii_case("inprocess")) + .unwrap_or(false) +} + +/// Skip guard for E2E tests exercising features the in-process (FFI) transport does not +/// support (the runtime loads into the shared host process). Returns `true` — and logs — +/// when running in-process so the caller can `return` early; such tests remain covered +/// by the default (stdio) transport. See . +pub fn skip_inprocess(reason: &str) -> bool { + if is_inprocess_default() { + eprintln!("skipping test over the in-process (FFI) transport: {reason}"); + true + } else { + false + } +} + +/// Mirrors an [`E2eContext`]'s environment onto the real process environment for the +/// in-process transport, whose worker inherits this process's ambient environment +/// rather than a per-client env block. Restores the previous values on drop. Only the +/// in-process transport needs this; for stdio/tcp the environment is handed to the +/// spawned child directly. Auth flows via GH_TOKEN/GITHUB_TOKEN and HMAC is disabled so +/// host-side auth resolution picks the token the replay snapshots expect. +struct InProcessEnvGuard { + saved: Vec<(OsString, Option)>, + previous_cwd: PathBuf, +} + +impl InProcessEnvGuard { + /// Returns `Some` guard (having applied the env) when in-process, else `None`. + fn activate(ctx: &E2eContext) -> Option { + if !is_inprocess_default() { + return None; + } + let mut pairs: Vec<(OsString, OsString)> = ctx.environment(); + pairs.retain(|(key, _)| { + key.as_os_str() != OsStr::new("COPILOT_HMAC_KEY") + && key.as_os_str() != OsStr::new("CAPI_HMAC_KEY") + }); + pairs.push(("COPILOT_SDK_AUTH_TOKEN".into(), "".into())); + pairs.push(( + "COPILOT_CLI_PATH".into(), + ctx.cli_path.clone().into_os_string(), + )); + // Some tests opt into gated runtime APIs via per-client `options.env`, which the + // in-process transport does not pass to the shared native runtime (see issue #1934). + // These are process-global runtime gates (not per-client behavior), so applying + // them to the host process for the serial in-process suite is equivalent and + // inert for tests that don't exercise the gated API. + pairs.push(("COPILOT_ALLOW_GET_PROVIDER_ENDPOINT".into(), "true".into())); + pairs.push(( + "COPILOT_EXP_COPILOT_CLI_WEBSOCKET_RESPONSES".into(), + "true".into(), + )); + pairs.push(( + "COPILOT_EXP_COPILOT_CLI_SESSION_BASED_SUBAGENTS".into(), + "true".into(), + )); + + let mut saved: Vec<(OsString, Option)> = Vec::new(); + for (key, value) in &pairs { + saved.push((key.clone(), std::env::var_os(key))); + // SAFETY: the E2E suite runs serially in-process (concurrency 1), so no + // other thread races these process-wide env mutations. + unsafe { std::env::set_var(key, value) }; + } + for key in ["COPILOT_HMAC_KEY", "CAPI_HMAC_KEY"] { + let key = OsString::from(key); + saved.push((key.clone(), std::env::var_os(&key))); + // SAFETY: as above, the in-process suite is serialized. + unsafe { std::env::remove_var(key) }; + } + let previous_cwd = std::env::current_dir().expect("read in-process test cwd"); + std::env::set_current_dir(ctx.work_dir()).expect("set in-process test cwd"); + Some(Self { + saved, + previous_cwd, + }) + } +} + +impl Drop for InProcessEnvGuard { + fn drop(&mut self) { + std::env::set_current_dir(&self.previous_cwd).expect("restore in-process test cwd"); + for (key, previous) in self.saved.iter().rev() { + // SAFETY: as in `activate` — serial execution in-process. + match previous { + Some(value) => unsafe { std::env::set_var(key, value) }, + None => unsafe { std::env::remove_var(key) }, + } + } + } +} + pub fn get_system_message(exchange: &serde_json::Value) -> String { exchange .get("request") @@ -617,11 +753,15 @@ fn cli_path(repo_root: &Path) -> std::io::Result { )) } +#[allow(deprecated)] fn client_options_for_cli( cli_path: &Path, cwd: &Path, env: Vec<(OsString, OsString)>, ) -> ClientOptions { + if is_inprocess_default() { + return ClientOptions::new(); + } let options = ClientOptions::new() .with_cwd(cwd) .with_env(env) diff --git a/rust/tests/e2e/telemetry.rs b/rust/tests/e2e/telemetry.rs index 38bf4a404a..f6905427ae 100644 --- a/rust/tests/e2e/telemetry.rs +++ b/rust/tests/e2e/telemetry.rs @@ -13,6 +13,11 @@ use super::support::{assistant_message_content, with_e2e_context}; #[tokio::test] async fn should_export_file_telemetry_for_sdk_interactions() { + // Telemetry lowers to environment variables the in-process worker cannot receive + // per-client; covered by the default (stdio) transport. See issue #1934. + if super::support::skip_inprocess("telemetry configuration is not honored in-process") { + return; + } with_e2e_context( "telemetry", "should_export_file_telemetry_for_sdk_interactions", diff --git a/rust/tests/e2e/tool_results.rs b/rust/tests/e2e/tool_results.rs index a6047007ff..e6e62643fa 100644 --- a/rust/tests/e2e/tool_results.rs +++ b/rust/tests/e2e/tool_results.rs @@ -213,14 +213,7 @@ async fn recv_called(receiver: &mut mpsc::UnboundedReceiver<()>, description: &' } fn expanded(text: impl Into, result_type: impl Into) -> ToolResult { - ToolResult::Expanded(ToolResultExpanded { - text_result_for_llm: text.into(), - result_type: result_type.into(), - binary_results_for_llm: None, - session_log: None, - error: None, - tool_telemetry: None, - }) + ToolResult::Expanded(ToolResultExpanded::new(text, result_type)) } fn weather_tool() -> Tool { diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 2599ea6d3a..122ec475df 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -20,10 +20,10 @@ use github_copilot_sdk::session_events::{ McpOauthRequiredData, ReasoningSummary, SessionLimitsConfig, }; use github_copilot_sdk::types::{ - CloudSessionOptions, CloudSessionRepository, CommandContext, CommandDefinition, CommandHandler, - DeliveryMode, ElicitationRequest, ElicitationResult, ExitPlanModeData, ExtensionInfo, - MessageOptions, RequestId, SessionConfig, SessionId, SetModelOptions, Tool, ToolInvocation, - ToolResult, + CanvasProviderIdentity, CloudSessionOptions, CloudSessionRepository, CommandContext, + CommandDefinition, CommandHandler, DeliveryMode, ElicitationRequest, ElicitationResult, + ExitPlanModeData, ExtensionInfo, MessageOptions, RequestId, SessionConfig, SessionId, + SetModelOptions, Tool, ToolInvocation, ToolResult, }; use github_copilot_sdk::{Client, ContextTier, ErrorKind, ProtocolErrorKind, tool}; use serde_json::Value; @@ -720,7 +720,11 @@ async fn create_session_sends_canvas_wire_fields() { .with_canvases([test_canvas("counter")]) .with_request_canvas_renderer(true) .with_request_extensions(true) - .with_extension_info(ExtensionInfo::new("github-app", "counter-provider")), + .with_extension_info(ExtensionInfo::new("github-app", "counter-provider")) + .with_canvas_provider( + CanvasProviderIdentity::new("app:builtin:window-1") + .with_name("Built-in"), + ), ) .await .unwrap() @@ -741,6 +745,11 @@ async fn create_session_sends_canvas_wire_fields() { request["params"]["extensionInfo"]["name"], "counter-provider" ); + assert_eq!( + request["params"]["canvasProvider"]["id"], + "app:builtin:window-1" + ); + assert_eq!(request["params"]["canvasProvider"]["name"], "Built-in"); let id = request["id"].as_u64().unwrap(); let session_id = requested_session_id(&request).to_string(); @@ -3311,11 +3320,13 @@ async fn resume_session_sends_canvas_fields_and_captures_open_canvases() { .with_request_canvas_renderer(true) .with_request_extensions(true) .with_extension_info(ExtensionInfo::new("github-app", "counter-provider")) + .with_canvas_provider(CanvasProviderIdentity::new("app:builtin:window-1")) .with_open_canvases([OpenCanvasInstance { instance_id: "counter-1".to_string(), extension_id: "github-app:counter-provider".to_string(), extension_name: Some("Counter Provider".to_string()), canvas_id: "counter".to_string(), + icon: None, title: Some("Counter".to_string()), status: Some("ready".to_string()), url: Some("https://example.test/counter".to_string()), @@ -3335,6 +3346,14 @@ async fn resume_session_sends_canvas_fields_and_captures_open_canvases() { request["params"]["extensionInfo"]["name"], "counter-provider" ); + assert_eq!( + request["params"]["canvasProvider"]["id"], + "app:builtin:window-1" + ); + assert!( + request["params"]["canvasProvider"].get("name").is_none(), + "name should be omitted from the wire when None, not serialized as null" + ); assert_eq!( request["params"]["openCanvases"][0]["instanceId"], "counter-1" diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index c9a5f8237d..24ec217f5b 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -27,6 +27,7 @@ import { findSharedSchemaDefinitions, postProcessSchema, propagateInternalVisibility, + filterNodeByVisibility, resolveRef, resolveObjectSchema, resolveSchema, @@ -230,7 +231,8 @@ function stripDurationMillisecondsSuffix(name: string): string { } function toCSharpPropertyName(propName: string, schema: JSONSchema7): string { - return toPascalCase(isDurationProperty(schema) ? stripDurationMillisecondsSuffix(propName) : propName); + const normalizedName = propName.replace(/^_+/, "") || propName; + return toPascalCase(isDurationProperty(schema) ? stripDurationMillisecondsSuffix(normalizedName) : normalizedName); } function isSecondsDurationPropertyName(propName: string | undefined): boolean { @@ -1321,7 +1323,7 @@ function emitSessionEventEnvelopeProperty( export function generateSessionEventsCode(schema: JSONSchema7): string { generatedEnums.clear(); sessionDefinitions = collectDefinitionCollections(schema as Record); - const variants = extractEventVariants(schema); + const variants = extractEventVariants(schema).filter((variant) => !isSchemaInternal(variant.dataSchema)); const knownTypes = new Map(); const nestedClasses = new Map(); const enumOutput: string[] = []; @@ -1380,12 +1382,16 @@ namespace GitHub.Copilot; if (variant.eventExperimental) { pushExperimentalAttribute(lines); } - const variantVisibility = isSchemaInternal(variant.dataSchema) ? "internal" : "public"; - lines.push(`${variantVisibility} sealed partial class ${variant.className} : SessionEvent`, `{`); + lines.push(`public sealed partial class ${variant.className} : SessionEvent`, `{`); lines.push(` /// `); lines.push(` [JsonIgnore]`, ` public override string Type => "${variant.typeName}";`, ""); lines.push(` /// The ${escapeXml(variant.typeName)} event payload.`); - lines.push(` [JsonPropertyName("data")]`, ` ${variantVisibility} required ${variant.dataClassName} Data { get; set; }`, `}`, ""); + lines.push( + ` [JsonPropertyName("data")]`, + ` public required ${variant.dataClassName} Data { get; set; }`, + `}`, + "" + ); } // Data classes @@ -2485,11 +2491,23 @@ function generateRpcCode( let sessionRpcParts: string[] = []; if (schema.session) sessionRpcParts = emitSessionRpcClasses(schema.session, classes); + // Client handler surfaces (interfaces, handler properties, RPC registration) + // are only generated for public methods. Internal client methods (e.g. + // `hooks.invoke`) are runtime transport plumbing and must not surface any + // generated code — including their request/result DTOs, which would + // otherwise leak as `internal` types referenced by a `public` handler + // interface (CS0050/CS0051 inconsistent accessibility). let clientSessionParts: string[] = []; - if (schema.clientSession) clientSessionParts = emitClientSessionApiRegistration(schema.clientSession, classes); + if (schema.clientSession) { + const publicClientSession = filterNodeByVisibility(schema.clientSession, "public"); + if (publicClientSession) clientSessionParts = emitClientSessionApiRegistration(publicClientSession, classes); + } let clientGlobalParts: string[] = []; - if (schema.clientGlobal) clientGlobalParts = emitClientGlobalApiRegistration(schema.clientGlobal, classes); + if (schema.clientGlobal) { + const publicClientGlobal = filterNodeByVisibility(schema.clientGlobal, "public"); + if (publicClientGlobal) clientGlobalParts = emitClientGlobalApiRegistration(publicClientGlobal, classes); + } const lines: string[] = []; lines.push(`${COPYRIGHT} diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index 1e6cf0a42c..b1d7474b98 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -581,7 +581,8 @@ function extractGoEventVariants(schema: JSONSchema7): GoEventVariant[] { eventExperimental: isSchemaExperimental(variant), dataExperimental: isSchemaExperimental(dataSchema), }; - }); + }) + .filter((variant) => !isSchemaInternal(variant.dataSchema)); } function getGoSharedEventEnvelopeProperties(schema: JSONSchema7, ctx: GoCodegenCtx): GoEventEnvelopeProperty[] { diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index b330eb9a7c..5b343122b6 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -1669,7 +1669,8 @@ function extractPyEventVariants(schema: JSONSchema7): PyEventVariant[] { eventExperimental: isSchemaExperimental(variant), dataExperimental: isSchemaExperimental(dataSchema), }; - }); + }) + .filter((variant) => !isSchemaInternal(variant.dataSchema)); } function getPySharedEventEnvelopeProperties(schema: JSONSchema7, ctx: PyCodegenCtx): PyEventEnvelopeProperty[] { diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index 3a3ce39a2f..a04f5a21c5 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -1109,7 +1109,11 @@ function extractEventVariants(schema: JSONSchema7): EventVariant[] { dataExperimental: isSchemaExperimental(dataSchema), }; }) - .filter((v) => !EXCLUDED_EVENT_TYPES.has(v.typeName)); + .filter( + (v) => + !EXCLUDED_EVENT_TYPES.has(v.typeName) && + !isSchemaInternal(v.dataSchema), + ); } export function generateSessionEventsCode(schema: JSONSchema7): string { diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index 497c909ea5..f82e67abe6 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -37,6 +37,7 @@ import { isNodeFullyDeprecated, isVoidSchema, isSchemaExperimental, + isSchemaInternal, appendPropertyMarkerTagsToDescriptions, getEnumValueDescriptions, stripOpaqueJsonMarker, @@ -348,7 +349,39 @@ async function generateSessionEvents(schemaPath?: string): Promise { resolveSchema({ $ref: "#/definitions/SessionEvent" }, definitionCollections) ?? resolveSchema({ $ref: "#/$defs/SessionEvent" }, definitionCollections) ?? processed; - const schemaForCompile = withSharedDefinitions(sessionEvent, definitionCollections); + const excludedDefinitionNames = new Set(); + const publicVariants = (sessionEvent.anyOf ?? []).filter((variant) => { + const variantSchema = variant as JSONSchema7; + const resolvedVariant = resolveSchema(variantSchema, definitionCollections) ?? variantSchema; + const dataSchema = resolvedVariant.properties?.data as JSONSchema7 | undefined; + const resolvedData = dataSchema ? resolveSchema(dataSchema, definitionCollections) ?? dataSchema : undefined; + if (!isSchemaInternal(resolvedData)) { + return true; + } + + for (const ref of [variantSchema.$ref, dataSchema?.$ref]) { + const match = ref?.match(/^#\/(?:definitions|\$defs)\/([^/]+)$/); + if (match) excludedDefinitionNames.add(match[1]); + } + return false; + }); + const publicDefinitions = Object.fromEntries( + Object.entries(definitionCollections.definitions).filter(([name]) => !excludedDefinitionNames.has(name)) + ); + const publicDraftDefinitions = Object.fromEntries( + Object.entries(definitionCollections.$defs).filter(([name]) => !excludedDefinitionNames.has(name)) + ); + const publicSessionEvent = { ...sessionEvent, anyOf: publicVariants }; + if ("SessionEvent" in publicDefinitions) { + publicDefinitions.SessionEvent = publicSessionEvent; + } + if ("SessionEvent" in publicDraftDefinitions) { + publicDraftDefinitions.SessionEvent = publicSessionEvent; + } + const schemaForCompile = withSharedDefinitions( + publicSessionEvent, + { definitions: publicDefinitions, $defs: publicDraftDefinitions } + ); appendPropertyMarkerTagsToDescriptions(schemaForCompile); const ts = await compile(normalizeSchemaForTypeScript(schemaForCompile), "SessionEvent", { diff --git a/test/harness/anthropicMessagesAdapter.ts b/test/harness/anthropicMessagesAdapter.ts new file mode 100644 index 0000000000..acc74a2bf9 --- /dev/null +++ b/test/harness/anthropicMessagesAdapter.ts @@ -0,0 +1,396 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import type { ChatCompletion } from "openai/resources/chat/completions"; +import { + CanonicalMessage, + CanonicalToolCall, + formatSseEvent, + functionToolCalls, + isObject, + JsonObject, +} from "./modelProtocolAdapterShared"; + +export const anthropicMessagesEndpoint = "/v1/messages"; + +type CanonicalContentPart = + | { type: "text"; text: string } + | { type: "image_url"; image_url: { url: string } } + | { + type: "file"; + file: { file_data: string; filename?: string }; + }; + +type AnthropicContentBlock = + | { type: "text"; text: string; citations?: null } + | { + type: "image" | "document"; + source?: { type?: string; media_type?: string; data?: string }; + } + | { type: "tool_use"; id: string; name: string; input: unknown } + | { + type: "tool_result"; + tool_use_id?: string; + content?: string | Array<{ type?: string; text?: string }>; + }; + +type AnthropicMessageParam = { + role: "user" | "assistant"; + content: string | AnthropicContentBlock[]; +}; + +type AnthropicRequest = { + model: string; + messages: AnthropicMessageParam[]; + system?: string | Array<{ type?: string; text?: string }>; + max_tokens?: number; + temperature?: number; + top_p?: number; + stream?: boolean; + tools?: Array<{ + name: string; + description?: string; + input_schema?: JsonObject; + }>; + tool_choice?: + | { type: "auto" | "any" | "none" } + | { type: "tool"; name: string }; +}; + +type AnthropicStopReason = + | "end_turn" + | "max_tokens" + | "stop_sequence" + | "tool_use" + | "refusal"; + +export type AnthropicMessage = { + id: string; + type: "message"; + role: "assistant"; + content: Array< + | { type: "text"; text: string; citations: null } + | { type: "tool_use"; id: string; name: string; input: unknown } + >; + model: string; + stop_reason: AnthropicStopReason | null; + stop_sequence: string | null; + usage: { + input_tokens: number; + output_tokens: number; + cache_creation_input_tokens: number | null; + cache_read_input_tokens: number | null; + }; +}; + +const finishReasonToStopReason: Record = { + stop: "end_turn", + length: "max_tokens", + tool_calls: "tool_use", + function_call: "tool_use", + content_filter: "refusal", +}; + +export function anthropicMessagesRequestToChatCompletion( + requestBody: string, +): string { + const request = JSON.parse(requestBody) as AnthropicRequest; + const messages: CanonicalMessage[] = []; + + const system = anthropicSystemToString(request.system); + if (system) messages.push({ role: "system", content: system }); + + for (const message of request.messages) { + messages.push(...convertAnthropicMessage(message)); + } + + return JSON.stringify({ + model: request.model, + messages, + ...(request.max_tokens !== undefined + ? { max_tokens: request.max_tokens } + : {}), + ...(request.temperature !== undefined + ? { temperature: request.temperature } + : {}), + ...(request.top_p !== undefined ? { top_p: request.top_p } : {}), + ...(request.stream !== undefined ? { stream: request.stream } : {}), + ...(request.tools + ? { + tools: request.tools.map((tool) => ({ + type: "function", + function: { + name: tool.name, + ...(tool.description ? { description: tool.description } : {}), + parameters: tool.input_schema ?? { + type: "object", + properties: {}, + }, + }, + })), + } + : {}), + ...(request.tool_choice + ? { tool_choice: convertToolChoice(request.tool_choice) } + : {}), + }); +} + +function anthropicSystemToString( + system: AnthropicRequest["system"], +): string | undefined { + if (typeof system === "string") return system; + if (!Array.isArray(system)) return undefined; + return system + .map((block) => (typeof block.text === "string" ? block.text : "")) + .filter(Boolean) + .join("\n"); +} + +function convertAnthropicMessage( + message: AnthropicMessageParam, +): CanonicalMessage[] { + return message.role === "user" + ? convertAnthropicUserMessage(message) + : convertAnthropicAssistantMessage(message); +} + +function normalizeContent( + content: AnthropicMessageParam["content"], +): AnthropicContentBlock[] { + return typeof content === "string" + ? [{ type: "text", text: content }] + : content; +} + +function convertAnthropicUserMessage( + message: AnthropicMessageParam, +): CanonicalMessage[] { + const result: CanonicalMessage[] = []; + const contentParts: CanonicalContentPart[] = []; + + const flushUserContent = () => { + if (contentParts.length === 0) return; + const onlyText = contentParts.every((part) => part.type === "text"); + result.push({ + role: "user", + content: onlyText + ? contentParts + .map((part) => (part.type === "text" ? part.text : "")) + .join("\n") + : [...contentParts], + }); + contentParts.length = 0; + }; + + for (const block of normalizeContent(message.content)) { + if (block.type === "text") { + contentParts.push({ type: "text", text: block.text }); + } else if ( + (block.type === "image" || block.type === "document") && + block.source?.type === "base64" && + block.source.data + ) { + const dataUrl = `data:${ + block.source.media_type ?? + (block.type === "image" ? "image/png" : "application/pdf") + };base64,${block.source.data}`; + contentParts.push( + block.type === "image" + ? { type: "image_url", image_url: { url: dataUrl } } + : { type: "file", file: { file_data: dataUrl } }, + ); + } else if (block.type === "tool_result") { + flushUserContent(); + result.push({ + role: "tool", + tool_call_id: block.tool_use_id ?? "", + content: anthropicToolResultContent(block.content), + }); + } + } + + flushUserContent(); + return result; +} + +function convertAnthropicAssistantMessage( + message: AnthropicMessageParam, +): CanonicalMessage[] { + const text: string[] = []; + const toolCalls: CanonicalToolCall[] = []; + for (const block of normalizeContent(message.content)) { + if (block.type === "text") { + text.push(block.text); + } else if (block.type === "tool_use") { + toolCalls.push({ + id: block.id, + type: "function", + function: { + name: block.name, + arguments: JSON.stringify(block.input ?? {}), + }, + }); + } + } + + return [ + { + role: "assistant", + content: text.length ? text.join("") : null, + ...(toolCalls.length ? { tool_calls: toolCalls } : {}), + }, + ]; +} + +function anthropicToolResultContent(content: unknown): string { + if (typeof content === "string") return content; + if (!Array.isArray(content)) return ""; + return content + .map((part) => + isObject(part) && typeof part.text === "string" ? part.text : "", + ) + .filter(Boolean) + .join("\n"); +} + +function convertToolChoice( + choice: NonNullable, +): unknown { + switch (choice.type) { + case "auto": + return "auto"; + case "any": + return "required"; + case "none": + return "none"; + case "tool": + return { type: "function", function: { name: choice.name } }; + } +} + +export function chatCompletionResponseToAnthropicMessage( + response: ChatCompletion, +): AnthropicMessage { + const content: AnthropicMessage["content"] = []; + for (const choice of response.choices) { + if (choice.message.content) { + content.push({ + type: "text", + text: choice.message.content, + citations: null, + }); + } + for (const toolCall of functionToolCalls(choice.message)) { + content.push({ + type: "tool_use", + id: toolCall.id, + name: toolCall.function.name, + input: safeParseJson(toolCall.function.arguments), + }); + } + } + + const finishReason = response.choices.at(-1)?.finish_reason; + return { + id: response.id, + type: "message", + role: "assistant", + content, + model: response.model, + stop_reason: finishReason + ? (finishReasonToStopReason[finishReason] ?? null) + : null, + stop_sequence: null, + usage: { + input_tokens: response.usage?.prompt_tokens ?? 0, + output_tokens: response.usage?.completion_tokens ?? 0, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + }, + }; +} + +export function chatCompletionResponseToAnthropicSseChunks( + response: ChatCompletion, +): string[] { + const message = chatCompletionResponseToAnthropicMessage(response); + const chunks = [ + formatSseEvent("message_start", { + type: "message_start", + message: { + ...message, + content: [], + stop_reason: null, + usage: { ...message.usage, output_tokens: 1 }, + }, + }), + ]; + + for (let index = 0; index < message.content.length; index++) { + const block = message.content[index]; + if (block.type === "text") { + chunks.push( + formatSseEvent("content_block_start", { + type: "content_block_start", + index, + content_block: { type: "text", text: "", citations: null }, + }), + formatSseEvent("content_block_delta", { + type: "content_block_delta", + index, + delta: { type: "text_delta", text: block.text }, + }), + ); + } else { + chunks.push( + formatSseEvent("content_block_start", { + type: "content_block_start", + index, + content_block: { + type: "tool_use", + id: block.id, + name: block.name, + input: {}, + }, + }), + formatSseEvent("content_block_delta", { + type: "content_block_delta", + index, + delta: { + type: "input_json_delta", + partial_json: JSON.stringify(block.input ?? {}), + }, + }), + ); + } + chunks.push( + formatSseEvent("content_block_stop", { + type: "content_block_stop", + index, + }), + ); + } + + chunks.push( + formatSseEvent("message_delta", { + type: "message_delta", + delta: { + stop_reason: message.stop_reason, + stop_sequence: message.stop_sequence, + }, + usage: { output_tokens: message.usage.output_tokens }, + }), + formatSseEvent("message_stop", { type: "message_stop" }), + ); + return chunks; +} + +function safeParseJson(value: string): unknown { + try { + return JSON.parse(value); + } catch { + return {}; + } +} diff --git a/test/harness/modelProtocolAdapterShared.ts b/test/harness/modelProtocolAdapterShared.ts new file mode 100644 index 0000000000..1f879da5d0 --- /dev/null +++ b/test/harness/modelProtocolAdapterShared.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +export type JsonObject = Record; + +export type CanonicalToolCall = { + id: string; + type: "function"; + function: { name: string; arguments: string }; +}; + +export type CanonicalMessage = { + role: "system" | "user" | "assistant" | "tool"; + content?: string | unknown[] | null; + tool_call_id?: string; + tool_calls?: CanonicalToolCall[]; +}; + +export function functionToolCalls(message: unknown): CanonicalToolCall[] { + if (!isObject(message) || !Array.isArray(message.tool_calls)) return []; + return message.tool_calls.filter( + (toolCall): toolCall is CanonicalToolCall => + isObject(toolCall) && + typeof toolCall.id === "string" && + toolCall.type === "function" && + isObject(toolCall.function) && + typeof toolCall.function.name === "string" && + typeof toolCall.function.arguments === "string", + ); +} + +export function formatSseEvent(type: string, data: unknown): string { + return `event: ${type}\ndata: ${JSON.stringify(data)}\n\n`; +} + +export function isObject(value: unknown): value is JsonObject { + return value !== null && typeof value === "object" && !Array.isArray(value); +} diff --git a/test/harness/modelProtocolAdapters.test.ts b/test/harness/modelProtocolAdapters.test.ts new file mode 100644 index 0000000000..ddb6fe40bf --- /dev/null +++ b/test/harness/modelProtocolAdapters.test.ts @@ -0,0 +1,641 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import type { ChatCompletion } from "openai/resources/chat/completions"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import yaml from "yaml"; +import { + anthropicMessagesRequestToChatCompletion, + chatCompletionResponseToAnthropicMessage, + chatCompletionResponseToAnthropicSseChunks, +} from "./anthropicMessagesAdapter"; +import { + chatCompletionResponseToResponsesApiMessage, + chatCompletionResponseToResponsesApiSseChunks, + responsesApiRequestToChatCompletion, +} from "./responsesApiAdapter"; +import { + NormalizedData, + ReplayBackend, + ReplayingCapiProxy, +} from "./replayingCapiProxy"; + +type ByokBackend = Exclude; + +const backends: ReplayBackend[] = [ + "capi", + "anthropic-messages", + "openai-responses", + "openai-completions", +]; + +const endpoints: Record = { + capi: "/chat/completions", + "anthropic-messages": "/v1/messages", + "openai-responses": "/responses", + "openai-completions": "/chat/completions", +}; + +const models: Record = { + capi: "gpt-4.1", + "anthropic-messages": "claude-sonnet-4.5", + "openai-responses": "gpt-4.1", + "openai-completions": "gpt-4.1", +}; + +const completionWithTool: ChatCompletion = { + id: "completion-1", + object: "chat.completion", + created: 123, + model: "test-model", + choices: [ + { + index: 0, + message: { + role: "assistant", + content: "Calling a tool", + refusal: null, + tool_calls: [ + { + id: "call-1", + type: "function", + function: { name: "lookup", arguments: '{"value":42}' }, + }, + ], + }, + logprobs: null, + finish_reason: "tool_calls", + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + }, +}; + +function requestFor( + backend: ReplayBackend, + prompt: string, +): Record { + const model = models[backend]; + switch (backend) { + case "anthropic-messages": + return { + model, + system: "Be helpful", + messages: [{ role: "user", content: prompt }], + max_tokens: 128, + }; + case "openai-responses": + return { + model, + instructions: "Be helpful", + input: [ + { + type: "message", + role: "user", + content: [{ type: "input_text", text: prompt }], + }, + ], + }; + case "capi": + case "openai-completions": + return { + model, + messages: [ + { role: "system", content: "Be helpful" }, + { role: "user", content: prompt }, + ], + }; + } +} + +async function postJson( + proxyUrl: string, + endpoint: string, + body: unknown, +): Promise { + return fetch(`${proxyUrl}${endpoint}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); +} + +describe("Anthropic Messages adapter", () => { + test("normalizes messages, binary content, and tools", () => { + const result = JSON.parse( + anthropicMessagesRequestToChatCompletion( + JSON.stringify({ + model: "test-model", + system: [{ type: "text", text: "Be helpful" }], + messages: [ + { + role: "user", + content: [ + { type: "text", text: "Inspect this" }, + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "AQID", + }, + }, + ], + }, + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "call-1", + name: "lookup", + input: { value: 42 }, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "call-1", + content: "found", + }, + ], + }, + ], + tools: [ + { + name: "lookup", + description: "Find a value", + input_schema: { type: "object" }, + }, + ], + stream: true, + }), + ), + ) as { + messages: Array>; + tools: Array>; + stream: boolean; + }; + + expect(result.messages.map((message) => message.role)).toEqual([ + "system", + "user", + "assistant", + "tool", + ]); + expect(result.messages[1].content).toEqual([ + { type: "text", text: "Inspect this" }, + { + type: "image_url", + image_url: { url: "data:image/png;base64,AQID" }, + }, + ]); + expect(result.messages[2].tool_calls).toEqual([ + { + id: "call-1", + type: "function", + function: { name: "lookup", arguments: '{"value":42}' }, + }, + ]); + expect(result.messages[3]).toMatchObject({ + tool_call_id: "call-1", + content: "found", + }); + expect(result.tools).toHaveLength(1); + expect(result.stream).toBe(true); + }); + + test("renders JSON and streaming tool responses", () => { + const message = + chatCompletionResponseToAnthropicMessage(completionWithTool); + expect(message.stop_reason).toBe("tool_use"); + expect(message.usage).toMatchObject({ + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + }); + expect(message.content.map((block) => block.type)).toEqual([ + "text", + "tool_use", + ]); + + const stream = + chatCompletionResponseToAnthropicSseChunks(completionWithTool).join(""); + expect(stream).toContain("event: message_start"); + expect(stream).toContain("event: content_block_delta"); + expect(stream).toContain("event: message_stop"); + }); + + test("combines tools from multiple canonical choices", () => { + const secondChoice = structuredClone(completionWithTool.choices[0]); + secondChoice.message.content = null; + secondChoice.message.tool_calls![0] = { + id: "call-2", + type: "function", + function: { name: "inspect", arguments: '{"path":"file.txt"}' }, + }; + + const message = chatCompletionResponseToAnthropicMessage({ + ...completionWithTool, + choices: [completionWithTool.choices[0], secondChoice], + }); + expect( + message.content + .filter((block) => block.type === "tool_use") + .map((block) => block.name), + ).toEqual(["lookup", "inspect"]); + }); +}); + +describe("OpenAI Responses adapter", () => { + test("normalizes messages, binary content, and tools", () => { + const result = JSON.parse( + responsesApiRequestToChatCompletion( + JSON.stringify({ + model: "test-model", + instructions: "Be helpful", + input: [ + { + type: "message", + role: "user", + content: [ + { type: "input_text", text: "Inspect this" }, + { + type: "input_image", + image_url: "data:image/png;base64,AQID", + }, + ], + }, + { + type: "function_call", + call_id: "call-1", + name: "lookup", + arguments: '{"value":42}', + }, + { + type: "function_call_output", + call_id: "call-1", + output: "found", + }, + ], + tools: [ + { + type: "function", + name: "lookup", + parameters: { type: "object" }, + }, + ], + }), + ), + ) as { + messages: Array>; + tools: Array>; + }; + + expect(result.messages.map((message) => message.role)).toEqual([ + "system", + "user", + "assistant", + "tool", + ]); + expect(result.messages[1].content).toEqual([ + { type: "text", text: "Inspect this" }, + { + type: "image_url", + image_url: { url: "data:image/png;base64,AQID" }, + }, + ]); + expect(result.messages[2].tool_calls).toEqual([ + { + id: "call-1", + type: "function", + function: { name: "lookup", arguments: '{"value":42}' }, + }, + ]); + expect(result.messages[3]).toMatchObject({ + tool_call_id: "call-1", + content: "found", + }); + expect(result.tools).toHaveLength(1); + }); + + test("renders JSON and streaming tool responses", () => { + const response = + chatCompletionResponseToResponsesApiMessage(completionWithTool); + const nextResponse = + chatCompletionResponseToResponsesApiMessage(completionWithTool); + expect(response).toMatchObject({ + object: "response", + created_at: completionWithTool.created, + status: "completed", + incomplete_details: null, + error: null, + }); + expect(response.output[0].id).not.toBe(nextResponse.output[0].id); + expect(response.output.map((item) => item.type)).toEqual([ + "message", + "function_call", + ]); + + const chunks = + chatCompletionResponseToResponsesApiSseChunks(completionWithTool); + const events = chunks.map( + (chunk) => + JSON.parse(chunk.split("\ndata: ")[1]) as Record, + ); + const stream = chunks.join(""); + expect(stream).toContain("event: response.created"); + expect(stream).toContain("event: response.in_progress"); + expect(stream).toContain("event: response.output_text.delta"); + expect(stream).toContain('"sequence_number":0'); + expect(stream).toContain("event: response.completed"); + + expect(events[0]).toMatchObject({ + type: "response.created", + response: { status: "in_progress", output: [] }, + }); + expect(events[1]).toMatchObject({ + type: "response.in_progress", + response: { status: "in_progress", output: [] }, + }); + + const addedItems = events.filter( + (event) => event.type === "response.output_item.added", + ); + expect(addedItems).toMatchObject([ + { + item: { + type: "message", + status: "in_progress", + content: [], + }, + }, + { + item: { + type: "function_call", + status: "in_progress", + arguments: "", + }, + }, + ]); + expect( + events.find((event) => event.type === "response.content_part.added"), + ).toMatchObject({ + part: { type: "output_text", text: "" }, + }); + + const completedItems = events.filter( + (event) => event.type === "response.output_item.done", + ); + expect(completedItems).toMatchObject([ + { + item: { + type: "message", + status: "completed", + content: [{ type: "output_text", text: "Calling a tool" }], + }, + }, + { + item: { + type: "function_call", + status: "completed", + arguments: '{"value":42}', + }, + }, + ]); + }); +}); + +describe("protocol-aware replay", () => { + let tempDir: string; + let workDir: string; + let cachePath: string; + + async function writeSnapshot( + messages: NormalizedData["conversations"][number]["messages"], + ): Promise { + await writeFile( + cachePath, + yaml.stringify({ + models: ["captured-capi-model"], + conversations: [{ messages }], + } satisfies NormalizedData), + ); + } + + async function withProxy( + backend: ReplayBackend, + action: (proxyUrl: string) => Promise, + ): Promise { + const proxy = new ReplayingCapiProxy( + "http://localhost:9999", + cachePath, + workDir, + ); + const proxyUrl = await proxy.start(); + await proxy.updateConfig({ filePath: cachePath, workDir, backend }); + try { + await action(proxyUrl); + } finally { + await proxy.stop(true); + } + } + + beforeEach(async () => { + tempDir = await mkdtemp(path.join(os.tmpdir(), "protocol-replay-")); + workDir = path.join(tempDir, "work"); + cachePath = path.join(tempDir, "cache.yaml"); + await writeSnapshot([ + { role: "system", content: "${system}" }, + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi there!" }, + ]); + }); + + afterEach(async () => { + await rm(tempDir, { recursive: true, force: true }); + }); + + test.each(backends)( + "replays one model-independent snapshot through %s", + async (backend) => { + await withProxy(backend, async (proxyUrl) => { + const response = await postJson( + proxyUrl, + endpoints[backend], + requestFor(backend, "Hello"), + ); + expect(response.status).toBe(200); + const body = (await response.json()) as Record; + expect(body.model).toBe(models[backend]); + + const exchanges = (await ( + await fetch(`${proxyUrl}/exchanges`) + ).json()) as Array<{ + request: { + model: string; + messages: Array<{ role: string; content: unknown }>; + }; + response?: unknown; + }>; + expect(exchanges).toHaveLength(1); + expect(exchanges[0].request.model).toBe(models[backend]); + expect(exchanges[0].request.messages.at(-1)).toEqual({ + role: "user", + content: "Hello", + }); + if ( + backend === "anthropic-messages" || + backend === "openai-responses" + ) { + expect(exchanges[0].response).toBeUndefined(); + } + }); + }, + ); + + test("does not rewrite canonical snapshots after BYOK replay", async () => { + const original = await readFile(cachePath, "utf8"); + const proxy = new ReplayingCapiProxy( + "http://localhost:9999", + cachePath, + workDir, + ); + const proxyUrl = await proxy.start(); + await proxy.updateConfig({ + filePath: cachePath, + workDir, + backend: "openai-responses", + }); + + let stopped = false; + try { + const response = await postJson( + proxyUrl, + endpoints["openai-responses"], + requestFor("openai-responses", "Hello"), + ); + expect(response.status).toBe(200); + await proxy.stop(); + stopped = true; + expect(await readFile(cachePath, "utf8")).toBe(original); + } finally { + if (!stopped) await proxy.stop(true); + } + }); + + test.each(["openai-responses", "openai-completions"] as const)( + "coalesces adjacent user messages from %s", + async (backend) => { + await writeSnapshot([ + { role: "system", content: "${system}" }, + { role: "user", content: "Hook context" }, + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi there!" }, + ]); + const request = requestFor(backend, "Hello"); + const hook = + "Hook context\n\n\n2026-01-01T00:00:00Z\n\n"; + if (backend === "openai-responses") { + (request.input as unknown[]).unshift({ + type: "message", + role: "user", + content: [{ type: "input_text", text: hook }], + }); + } else { + (request.messages as unknown[]).splice(1, 0, { + role: "user", + content: hook, + }); + } + + await withProxy(backend, async (proxyUrl) => { + const response = await postJson( + proxyUrl, + endpoints[backend], + request, + ); + expect(response.status).toBe(200); + }); + }, + ); + + test("normalizes Anthropic spacing between adjacent user turns", async () => { + await writeSnapshot([ + { role: "system", content: "${system}" }, + { role: "user", content: "First prompt" }, + { role: "user", content: "Recovery prompt" }, + { role: "assistant", content: "Recovered" }, + ]); + await withProxy("anthropic-messages", async (proxyUrl) => { + const response = await postJson( + proxyUrl, + endpoints["anthropic-messages"], + requestFor( + "anthropic-messages", + "First prompt\n\n\n\n\nRecovery prompt", + ), + ); + expect(response.status).toBe(200); + }); + }); + + test.each(backends)( + "replays compaction responses through %s", + async (backend) => { + await writeSnapshot([ + { role: "system", content: "${system}" }, + { role: "user", content: "${compaction_prompt}" }, + { + role: "assistant", + content: + "CompactedHistoryCheckpoint", + }, + ]); + await withProxy(backend, async (proxyUrl) => { + const response = await postJson( + proxyUrl, + endpoints[backend], + requestFor(backend, "${compaction_prompt}"), + ); + expect(response.status).toBe(200); + const body = JSON.stringify(await response.json()); + expect(body).toContain(""); + expect(body).toContain(""); + expect(body).toContain(""); + }); + }, + ); + + test("rejects an inference request over the wrong protocol", async () => { + await withProxy("anthropic-messages", async (proxyUrl) => { + const response = await postJson( + proxyUrl, + endpoints["openai-completions"], + requestFor("openai-completions", "Hello"), + ); + expect(response.status).toBe(400); + await expect(response.text()).resolves.toContain("protocol_mismatch"); + }); + }); + + test("keeps foreign model endpoints unavailable in CAPI mode", async () => { + await withProxy("capi", async (proxyUrl) => { + const response = await postJson( + proxyUrl, + endpoints["openai-responses"], + requestFor("openai-responses", "Hello"), + ); + expect(response.status).toBe(404); + }); + }); +}); diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index 46f3db29b8..1a8462d661 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^1.0.69-2", + "@github/copilot": "^1.0.71", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", @@ -501,9 +501,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.69-2.tgz", - "integrity": "sha512-D/fvtb8RZVL0jbDjU5k7M2J08mr2eB0n7+NwUAJw/9+s1lmZBN6F3OqKh83Uo03d8oLW32ITJhqoxvs85pyiLw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.71.tgz", + "integrity": "sha512-F3axBi+sXSLYDJbxCBW36bM6MYKNC2rlyAf3Ivo/MjiHKKJW7j5AmaR1IRYS9Gt8r9mxOwWFM1cJFA+CuLaR8g==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "dependencies": { @@ -513,20 +513,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.69-2", - "@github/copilot-darwin-x64": "1.0.69-2", - "@github/copilot-linux-arm64": "1.0.69-2", - "@github/copilot-linux-x64": "1.0.69-2", - "@github/copilot-linuxmusl-arm64": "1.0.69-2", - "@github/copilot-linuxmusl-x64": "1.0.69-2", - "@github/copilot-win32-arm64": "1.0.69-2", - "@github/copilot-win32-x64": "1.0.69-2" + "@github/copilot-darwin-arm64": "1.0.71", + "@github/copilot-darwin-x64": "1.0.71", + "@github/copilot-linux-arm64": "1.0.71", + "@github/copilot-linux-x64": "1.0.71", + "@github/copilot-linuxmusl-arm64": "1.0.71", + "@github/copilot-linuxmusl-x64": "1.0.71", + "@github/copilot-win32-arm64": "1.0.71", + "@github/copilot-win32-x64": "1.0.71" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.69-2.tgz", - "integrity": "sha512-643mEEP/0FfZQvxNmg9UquVJv01gJ2QTdi2qabT/cPX2jLpgrPRjRvikX/XewAzGSUSzy4pyx5qyDYIr7TjUcw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.71.tgz", + "integrity": "sha512-mEWzyqbqRAWgyU7i2uuSRoVPx/TwaFQX0nZmw0bc30aJ0BnO7cy2kYQyCHw8ykmf/tfxT0xauZ6k0BOFmWizzQ==", "cpu": [ "arm64" ], @@ -541,9 +541,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.69-2.tgz", - "integrity": "sha512-Lx4+8lLJdI8bhA/pLWcRMuae3nxM7SivhWpS5lWsQyPrsdF5g1vYPtmo7ip3hc65jOxpzVWImc9FgQ8d5fKvOA==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.71.tgz", + "integrity": "sha512-Md9yEg406OBVBx3w4PeEj62TubulVLBcHleqmCoOoUmPgUxPZotUbrqz3rtbzADbXfrrD7JWvVsbd2UiNL194w==", "cpu": [ "x64" ], @@ -558,9 +558,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.69-2.tgz", - "integrity": "sha512-IheFTABphOz4QIqTfN0sLwF57yZtOm0IWJbgUtQe9WkiduD9L8Ii1EtJKbpVPygIgwX4LbcYTCyCMglZjLPliw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.71.tgz", + "integrity": "sha512-ykLJYOqBj3jRB5IJCDugLClAqbr7DmtTbUjlNY7+Jdq/n6i+d7xUQGclf1IWL5gnxbGQVAf+zkToD+sRM389Kg==", "cpu": [ "arm64" ], @@ -575,9 +575,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.69-2.tgz", - "integrity": "sha512-FHMSL5sUqH55U2kyaRaUGNGguyr5SI2ce5FriyPnVQWvYUSuRbdfEo9mC8jeONecD0sO0FQuWwbMk2/JkQOHTA==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.71.tgz", + "integrity": "sha512-pC0FNHG+BBwZd6yZlM85kkAGN+uJhM6o+THi76N2GnnSxmw7+remb1mvYxdgRVbdCm+LBUIbCKRWJLuMwrfb6A==", "cpu": [ "x64" ], @@ -592,9 +592,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.69-2.tgz", - "integrity": "sha512-c8yvsxEUNjwaQOU8zO0VFg7LWqTDDvx2SV3rzNz4FSCxTZf0SmcZw6TczsTauKO/0hAq5lfLH4d1pJkTLQ62Zw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.71.tgz", + "integrity": "sha512-hBmDljFTjacxqZTasCEy43H8EIzuXB/hHEBBCMFjhB9J00nIxsO6Dh0woTifKpx7knTYZdpTjjca3D0pAoZlUA==", "cpu": [ "arm64" ], @@ -609,9 +609,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.69-2.tgz", - "integrity": "sha512-9OQVqN3muGyBePmDY7jGsfhGfCAqvauSZqoWZfX+n28YtZrXTXR7IfGSuzBwZq/isOhopaIplmOc19ZYIMeCEw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.71.tgz", + "integrity": "sha512-CfTXU8pa5dxRz22xQzoi3TiG1PJo9+WR8PRDiPSdkIBSyPJ1NvX87DJmfXjTgeAfR+wkjt/p0keDCaBBVhNmUA==", "cpu": [ "x64" ], @@ -626,9 +626,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.69-2.tgz", - "integrity": "sha512-eEKJid8T+tI70dn+2UL4s2b2AQcoHaAmaJ6x/7qTksdVuaII27vuVO/yE4wpWkhYl+yvI3Ml/nkavgrms7Y/Pw==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.71.tgz", + "integrity": "sha512-+HI1DokixXhHUahj06Fw67ZAigBuXKC58BFma4UJOGrQsDgwOSbqeTQHCw6vuymzjKlg3sactfsCUTaefkjscQ==", "cpu": [ "arm64" ], @@ -643,9 +643,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.69-2", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.69-2.tgz", - "integrity": "sha512-usFpfQudpOEft/OYQaJ+c0MJOgP9bXZ1vctx5mRgC6d8+0dIRbLCZ5rVuir1K7zyS246uYHZgO/t0sMQH7pOdQ==", + "version": "1.0.71", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.71.tgz", + "integrity": "sha512-02kXOBd9CwBbCaztuf71WYWn+uGapCuiaasomN4tcMH3HBVZ4gi3J0ZUoRcgcS80xh81uQyeBHbnUKzb/RE/9A==", "cpu": [ "x64" ], diff --git a/test/harness/package.json b/test/harness/package.json index f71bc5248a..18b19e21ac 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -14,7 +14,7 @@ "node": "^20.19.0 || >=22.12.0" }, "devDependencies": { - "@github/copilot": "^1.0.69-2", + "@github/copilot": "^1.0.71", "@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 8032265485..009a1e40a8 100644 --- a/test/harness/replayingCapiProxy.test.ts +++ b/test/harness/replayingCapiProxy.test.ts @@ -509,6 +509,47 @@ Always include PINEAPPLE_COCONUT_42. expect(toolMessages[1].content).toBe("[beta result]"); }); + test("collapses the available-tools list to a stable placeholder", async () => { + const requestBody = JSON.stringify({ + messages: [ + { role: "user", content: "Help me" }, + { + role: "assistant", + tool_calls: [ + { + id: "tc1", + type: "function", + function: { name: "report_intent", arguments: "{}" }, + }, + ], + }, + { + role: "tool", + tool_call_id: "tc1", + content: + "Tool 'report_intent' does not exist. Available tools that can be called are bash, read_bash, view, read_agent, list_agents, write_agent, grep, glob, task.", + }, + ], + }); + const responseBody = JSON.stringify({ + choices: [{ message: { role: "assistant", content: "Done" } }], + }); + + const outputPath = await createProxy([ + { url: "/chat/completions", requestBody, responseBody }, + ]); + + const result = await readYamlOutput(outputPath); + const toolMessage = result.conversations[0].messages.find( + (m) => m.role === "tool", + ); + // The whole enumeration collapses so snapshots stay stable as the built-in + // tool set evolves (e.g. write_agent being added). + expect(toolMessage?.content).toBe( + "Tool 'report_intent' does not exist. Available tools that can be called are ${available_tools}.", + ); + }); + test("normalizes read_agent timing metadata", async () => { const requestBody = JSON.stringify({ messages: [ @@ -827,6 +868,85 @@ Always include PINEAPPLE_COCONUT_42. } }); + test("matches available-tools results after the built-in tool set changes", async () => { + const cachePath = path.join(tempDir, "cache.yaml"); + // Legacy snapshot recorded before write_agent was a built-in tool: the + // enumeration frozen on disk still contains the older tool list. + const cacheContent = yaml.stringify({ + models: ["test-model"], + conversations: [ + { + messages: [ + { role: "system", content: "${system}" }, + { role: "user", content: "Report intent" }, + { + role: "assistant", + tool_calls: [ + { + id: "toolcall_0", + type: "function", + function: { name: "report_intent", arguments: "{}" }, + }, + ], + }, + { + role: "tool", + tool_call_id: "toolcall_0", + content: + "Tool 'report_intent' does not exist. Available tools that can be called are ${shell}, view, read_agent, list_agents, grep, glob, task.", + }, + { role: "assistant", content: "Done" }, + ], + }, + ], + } satisfies NormalizedData); + await writeFile(cachePath, cacheContent); + + const proxy = new ReplayingCapiProxy( + "http://localhost:9999", + cachePath, + workDir, + ); + const proxyUrl = await proxy.start(); + + try { + const response = await makeRequest(proxyUrl, "/chat/completions", { + body: { + model: "test-model", + messages: [ + { role: "system", content: "System prompt" }, + { role: "user", content: "Report intent" }, + { + role: "assistant", + tool_calls: [ + { + id: "runtime-call-id", + type: "function", + function: { name: "report_intent", arguments: "{}" }, + }, + ], + }, + { + role: "tool", + tool_call_id: "runtime-call-id", + // Newer runtime added write_agent to the built-in tool set. + content: + "Tool 'report_intent' does not exist. Available tools that can be called are bash, read_bash, view, read_agent, list_agents, write_agent, grep, glob, task.", + }, + ], + }, + }); + + expect(response.status).toBe(200); + expect( + (JSON.parse(response.body) as ChatCompletion).choices[0].message + .content, + ).toBe("Done"); + } finally { + await proxy.stop(); + } + }); + test("expands workdir placeholder in cached response", 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 740f8ab746..1c29fb7559 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { existsSync, appendFileSync } from "fs"; +import { appendFileSync, existsSync } from "fs"; import { mkdir, readFile, writeFile } from "fs/promises"; import type { ChatCompletion, @@ -19,10 +19,80 @@ import { CapturingHttpProxy, PerformRequestOptions, } from "./capturingHttpProxy"; +import { + anthropicMessagesEndpoint, + anthropicMessagesRequestToChatCompletion, + chatCompletionResponseToAnthropicMessage, + chatCompletionResponseToAnthropicSseChunks, +} from "./anthropicMessagesAdapter"; +import { + chatCompletionResponseToResponsesApiMessage, + chatCompletionResponseToResponsesApiSseChunks, + responsesApiRequestToChatCompletion, + responsesEndpoint, +} from "./responsesApiAdapter"; import { iife, ShellConfig, sleep } from "./util"; export const workingDirPlaceholder = "${workdir}"; const chatCompletionEndpoint = "/chat/completions"; +export type ReplayBackend = + | "capi" + | "anthropic-messages" + | "openai-responses" + | "openai-completions"; + +type ReplayProtocol = { + endpoint: string; + normalizeRequest?: (body: string) => string; + responseBody?: (response: ChatCompletion) => unknown; + responseChunks: (response: ChatCompletion) => string[]; + responseEndChunk?: string; + errorBody?: (code: string | undefined, message: string) => unknown; + canonicalResponse?: boolean; +}; + +const chatCompletionsProtocol = { + endpoint: chatCompletionEndpoint, + responseChunks: (response) => + convertToStreamingResponseChunks(response).map( + (chunk) => `data: ${JSON.stringify(chunk)}\n\n`, + ), + responseEndChunk: "data: [DONE]\n\n", + canonicalResponse: true, +} satisfies ReplayProtocol; + +const replayProtocols: Record = { + capi: chatCompletionsProtocol, + "openai-completions": { + ...chatCompletionsProtocol, + normalizeRequest: coalesceAdjacentUserMessages, + }, + "anthropic-messages": { + endpoint: anthropicMessagesEndpoint, + normalizeRequest: (body) => + coalesceAdjacentUserMessages( + anthropicMessagesRequestToChatCompletion(body), + ), + responseBody: chatCompletionResponseToAnthropicMessage, + responseChunks: chatCompletionResponseToAnthropicSseChunks, + errorBody: (code, message) => { + const type = code ?? "rate_limited"; + return { type: "error", error: { type, message } }; + }, + }, + "openai-responses": { + endpoint: responsesEndpoint, + normalizeRequest: (body) => + coalesceAdjacentUserMessages(responsesApiRequestToChatCompletion(body)), + responseBody: chatCompletionResponseToResponsesApiMessage, + responseChunks: chatCompletionResponseToResponsesApiSseChunks, + }, +}; + +const modelEndpoints = new Set( + Object.values(replayProtocols).map((protocol) => protocol.endpoint), +); + const shellConfig = process.platform === "win32" ? ShellConfig.powerShell : ShellConfig.bash; const normalizedToolNames: Record = { @@ -90,6 +160,7 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { filePath, workDir, testInfo, + backend: "capi", toolResultNormalizers: [...this.defaultToolResultNormalizers], }; } @@ -112,7 +183,10 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { // In CI mode (GITHUB_ACTIONS=true) we never write — the snapshots are read-only. // Otherwise tests that exercise only a subset of a multi-conversation snapshot // would silently overwrite the file with that subset, breaking subsequent runs. - if (this.state && process.env.GITHUB_ACTIONS !== "true") { + if ( + this.state?.backend === "capi" && + process.env.GITHUB_ACTIONS !== "true" + ) { await writeCapturesToDisk(this.exchanges, this.state); } @@ -120,6 +194,7 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { filePath: config.filePath, workDir: config.workDir, testInfo: config.testInfo, + backend: parseReplayBackend(config.backend), toolResultNormalizers: [...this.defaultToolResultNormalizers], }; @@ -133,15 +208,21 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { this.state.storedData = yaml.parse(content) as NormalizedData; normalizeToolResultOrder(this.state.storedData.conversations); normalizeStoredUserMessages(this.state.storedData.conversations); + normalizeStoredToolMessages(this.state.storedData.conversations); + normalizeStoredMessagesForBackend( + this.state.storedData.conversations, + this.state.backend, + ); } } async stop(skipWritingCache?: boolean): Promise { await super.stop(); - // In CI mode we never write — the snapshots are read-only. + // CAPI is the authoritative capture path. BYOK modes only verify that the + // same canonical snapshots replay through each provider protocol. if ( - this.state && + this.state?.backend === "capi" && !skipWritingCache && process.env.GITHUB_ACTIONS !== "true" ) { @@ -223,17 +304,21 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { options.requestOptions.path === "/exchanges" && options.requestOptions.method === "GET" ) { - const chatCompletionExchanges = this.exchanges.filter( - (e) => e.request.url === chatCompletionEndpoint, - ); + const protocol = + replayProtocols[this.state?.backend ?? "capi"]; const parsedExchanges = await Promise.all( - chatCompletionExchanges.map((e) => - parseHttpExchange( - e.request.body, - e.response?.body, - e.request.headers, + this.exchanges + .filter((exchange) => exchange.request.url === protocol.endpoint) + .map((exchange) => + parseHttpExchange( + protocol.normalizeRequest?.(exchange.request.body) ?? + exchange.request.body, + protocol.canonicalResponse + ? exchange.response?.body + : undefined, + exchange.request.headers, + ), ), - ), ); options.onResponseStart(200, {}); options.onData(Buffer.from(JSON.stringify(parsedExchanges))); @@ -335,16 +420,42 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { options.onResponseEnd(); return; } - - // Handle /chat/completions endpoint + const requestPath = options.requestOptions.path ?? ""; + const protocol = replayProtocols[state.backend]; if ( - state.storedData && - options.requestOptions.path === chatCompletionEndpoint && - options.body + modelEndpoints.has(requestPath) && + state.backend !== "capi" && + requestPath !== protocol.endpoint ) { + const message = `Expected ${protocol.endpoint} for backend ${state.backend}, received ${requestPath}`; + options.onResponseStart(400, { + "content-type": "application/json", + ...commonResponseHeaders, + }); + options.onData( + Buffer.from( + JSON.stringify({ + error: { type: "protocol_mismatch", message }, + }), + ), + ); + options.onResponseEnd(); + return; + } + + const isModelRequest = requestPath === protocol.endpoint; + // Every protocol enters the existing Chat Completions snapshot matcher. + const normalizedBody = + isModelRequest && options.body + ? (protocol.normalizeRequest?.(options.body) ?? options.body) + : options.body; + if (state.storedData && isModelRequest && normalizedBody) { + const streamingIsRequested = + (JSON.parse(normalizedBody) as { stream?: boolean }).stream === true; + const savedError = await findSavedChatCompletionError( state.storedData, - options.body, + normalizedBody, state.workDir, state.toolResultNormalizers, ); @@ -360,14 +471,12 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { options.onResponseStart(savedError.status, headers); options.onData( Buffer.from( - JSON.stringify({ - error: { - message: - savedError.message ?? "Rate limited by test snapshot", - type: savedError.code ?? "rate_limited", - code: savedError.code ?? "rate_limited", - }, - }), + JSON.stringify( + (protocol.errorBody ?? openAIErrorBody)( + savedError.code, + savedError.message ?? "Rate limited by test snapshot", + ), + ), ), ); options.onResponseEnd(); @@ -376,45 +485,19 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { const savedResponse = await findSavedChatCompletionResponse( state.storedData, - options.body, + normalizedBody, state.workDir, state.toolResultNormalizers, ); if (savedResponse) { - const streamingIsRequested = - options.body && - (JSON.parse(options.body) as { stream?: boolean }).stream === - true; - - if (streamingIsRequested) { - const headers = { - "content-type": "text/event-stream", - ...commonResponseHeaders, - }; - options.onResponseStart(200, headers); - for (const chunk of convertToStreamingResponseChunks( - savedResponse, - )) { - options.onData( - Buffer.from(`data: ${JSON.stringify(chunk)}\n\n`), - ); - if (this.slowStreaming) { - await sleep(100); - } - } - options.onData(Buffer.from("data: [DONE]\n\n")); - options.onResponseEnd(); - } else { - const body = JSON.stringify(savedResponse); - const headers = { - "content-type": "application/json", - ...commonResponseHeaders, - }; - options.onResponseStart(200, headers); - options.onData(Buffer.from(body)); - options.onResponseEnd(); - } + await this.respondWithProtocol( + options, + protocol, + savedResponse, + streamingIsRequested, + commonResponseHeaders, + ); return; } @@ -424,15 +507,11 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { if ( await isRequestOnlySnapshot( state.storedData, - options.body, + normalizedBody, state.workDir, state.toolResultNormalizers, ) ) { - const streamingIsRequested = - options.body && - (JSON.parse(options.body) as { stream?: boolean }).stream === - true; const headers = { "content-type": streamingIsRequested ? "text/event-stream" @@ -449,7 +528,7 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { // Beyond this point, we're only going to be able to supply responses in CI if we have a snapshot, // and we only store snapshots for chat completion. For anything else (e.g., custom-agents fetches), // return 404 so the CLI treats them as unavailable instead of erroring. - if (options.requestOptions.path !== chatCompletionEndpoint) { + if (!isModelRequest) { const headers = { "content-type": "application/json", "x-github-request-id": "proxy-not-found", @@ -465,13 +544,14 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { // Fallback to normal proxying if no cached response found // This implicitly captures the new exchange too const isCI = process.env.GITHUB_ACTIONS === "true"; - if (isCI) { + if (isCI || state.backend !== "capi") { await exitWithNoMatchingRequestError( options, state.testInfo, state.workDir, state.toolResultNormalizers, state.storedData, + normalizedBody, ); return; } @@ -481,6 +561,43 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { } }); } + + private async respondWithProtocol( + options: PerformRequestOptions, + protocol: ReplayProtocol, + response: ChatCompletion, + streaming: boolean, + commonHeaders: Record, + ): Promise { + if (!streaming) { + options.onResponseStart(200, { + "content-type": "application/json", + ...commonHeaders, + }); + options.onData( + Buffer.from( + JSON.stringify(protocol.responseBody?.(response) ?? response), + ), + ); + options.onResponseEnd(); + return; + } + + options.onResponseStart(200, { + "content-type": "text/event-stream", + ...commonHeaders, + }); + for (const chunk of protocol.responseChunks(response)) { + options.onData(Buffer.from(chunk)); + if (this.slowStreaming) { + await sleep(100); + } + } + if (protocol.responseEndChunk) { + options.onData(Buffer.from(protocol.responseEndChunk)); + } + options.onResponseEnd(); + } } async function writeCapturesToDisk( @@ -591,11 +708,12 @@ async function exitWithNoMatchingRequestError( workDir: string, toolResultNormalizers: ToolResultNormalizer[], storedData?: NormalizedData, + requestBody?: string, ) { let diagnostics: string; try { const normalized = await parseAndNormalizeRequest( - options.body, + requestBody ?? options.body, workDir, toolResultNormalizers, ); @@ -604,8 +722,11 @@ async function exitWithNoMatchingRequestError( let rawMessages: unknown[] = []; try { rawMessages = - (JSON.parse(options.body ?? "{}") as { messages?: unknown[] }) - .messages ?? []; + ( + JSON.parse(requestBody ?? options.body ?? "{}") as { + messages?: unknown[]; + } + ).messages ?? []; } catch { /* non-JSON body */ } @@ -774,6 +895,60 @@ async function transformHttpExchanges( return { models: Array.from(dedupedModels), conversations: dedupedExchanges }; } +function parseReplayBackend(value: unknown): ReplayBackend { + if (value === undefined || value === null || value === "") return "capi"; + if (typeof value === "string" && Object.hasOwn(replayProtocols, value)) { + return value as ReplayBackend; + } + throw new Error(`Unsupported replay backend: ${String(value)}`); +} + +function coalesceAdjacentUserMessages(requestBody: string): string { + const request = JSON.parse(requestBody) as { + messages?: Array<{ + role?: string; + content?: unknown; + [key: string]: unknown; + }>; + }; + if (!request.messages) return requestBody; + + const messages: NonNullable = []; + for (const message of request.messages) { + const previous = messages.at(-1); + if ( + previous?.role === "user" && + message.role === "user" && + typeof previous.content === "string" && + typeof message.content === "string" + ) { + previous.content = `${previous.content.trimEnd()}\n\n\n${message.content.trimStart()}`; + } else { + messages.push(message); + } + } + + for (const message of messages) { + if (message.role === "user" && typeof message.content === "string") { + message.content = normalizeUserMessage(message.content).replace( + /\n{5,}/g, + "\n\n\n", + ); + } + } + + request.messages = messages; + return JSON.stringify(request); +} + +function openAIErrorBody( + code: string | undefined, + message: string, +): unknown { + const type = code ?? "rate_limited"; + return { error: { message, type, code: type } }; +} + function normalizeFilenames( conversations: NormalizedConversation[], workDir: string, @@ -1079,6 +1254,67 @@ function normalizeStoredUserMessages(conversations: NormalizedConversation[]) { } } +function normalizeStoredMessagesForBackend( + conversations: NormalizedConversation[], + backend: ReplayBackend, +) { + if (backend === "capi") return; + + for (const conversation of conversations) { + conversation.messages = coalesceMessages( + conversation.messages, + backend !== "openai-completions", + ); + } +} + +function coalesceMessages( + messages: NormalizedMessage[], + coalesceAssistantMessages: boolean, +): NormalizedMessage[] { + const result: NormalizedMessage[] = []; + for (const message of messages) { + const previous = result.at(-1); + const shouldCoalesce = + previous?.role === message.role && + ((coalesceAssistantMessages && message.role === "assistant") || + message.role === "user"); + if (!shouldCoalesce) { + result.push(message); + continue; + } + + const separator = message.role === "user" ? "\n\n\n" : ""; + const previousContent = previous.content ?? ""; + const currentContent = message.content ?? ""; + const content = `${previousContent}${previousContent && currentContent ? separator : ""}${currentContent}`; + if (content) previous.content = content; + + const toolCalls = [ + ...(previous.tool_calls ?? []), + ...(message.tool_calls ?? []), + ]; + if (toolCalls.length) previous.tool_calls = toolCalls; + } + return result; +} + +// Re-normalizes the built-in tool enumeration in stored tool results at load +// time. Snapshots recorded before normalizeAvailableToolNames collapsed the +// whole list (or recorded against an older tool set) still contain the literal +// enumeration on disk; the result normalizers only run against live requests, +// so without this the stored side would keep the stale list and never match a +// request whose tool set has since changed. +function normalizeStoredToolMessages(conversations: NormalizedConversation[]) { + for (const conversation of conversations) { + for (const message of conversation.messages) { + if (message.role === "tool" && typeof message.content === "string") { + message.content = normalizeAvailableToolNames(message.content); + } + } + } +} + function normalizeSkillContextFrontmatter(content: string): string { // Runtime versions may include or omit SKILL.md metadata in the prompt context. return content.replace( @@ -1169,41 +1405,21 @@ function normalizeReadAgentTimings(result: string): string { .replace(/\bduration: \d+(?:\.\d+)?s\b/g, "duration: 0s"); } -// Maps the platform-specific shell tool family names to stable placeholders. -// On Windows the runtime exposes powershell/read_powershell/stop_powershell/..., -// on Linux/macOS it exposes bash/read_bash/stop_bash/.... Ordered so that the -// prefixed names are handled explicitly; \b boundaries keep bare names from -// matching inside the prefixed ones. -const shellToolFamilyReplacements: ReadonlyArray = [ - [/\bread_powershell\b/g, "${read_shell}"], - [/\bstop_powershell\b/g, "${stop_shell}"], - [/\blist_powershell\b/g, "${list_shell}"], - [/\bwrite_powershell\b/g, "${write_shell}"], - [/\bpowershell\b/g, "${shell}"], - [/\bread_bash\b/g, "${read_shell}"], - [/\bstop_bash\b/g, "${stop_shell}"], - [/\blist_bash\b/g, "${list_shell}"], - [/\bwrite_bash\b/g, "${write_shell}"], - [/\bbash\b/g, "${shell}"], -]; - -function normalizeShellToolFamilyNames(text: string): string { - let result = text; - for (const [pattern, replacement] of shellToolFamilyReplacements) { - result = result.replace(pattern, replacement); - } - return result; -} +// Stable placeholder for the built-in tool enumeration the runtime emits when a +// nonexistent tool is called (see normalizeAvailableToolNames). +export const availableToolsPlaceholder = "${available_tools}"; // When a model calls a tool that doesn't exist (e.g., the removed report_intent // tool), the runtime replies with "Available tools that can be called are ." -// The shell tool family names in that list are platform-specific, so normalize -// them to placeholders to keep snapshots matching across Windows/Linux/macOS. +// That enumeration is both platform-specific (shell tool family names differ +// across OSes) and runtime-version-specific (built-in tools such as write_agent +// are added or removed over time), so any test that trips this path would break +// whenever the tool set changes. Collapse the whole list to a stable placeholder +// so snapshots keep matching as the built-in tool set evolves. function normalizeAvailableToolNames(result: string): string { return result.replace( - /(Available tools that can be called are )([^.]*)/g, - (_full, prefix: string, list: string) => - prefix + normalizeShellToolFamilyNames(list), + /(Available tools that can be called are )[^.]*/g, + (_full, prefix: string) => prefix + availableToolsPlaceholder, ); } @@ -1546,6 +1762,7 @@ type ReplayingCapiProxyState = { filePath: string; workDir: string; testInfo?: { file: string; line?: number }; + backend: ReplayBackend; storedData?: NormalizedData | undefined; toolResultNormalizers: ToolResultNormalizer[]; }; diff --git a/test/harness/responsesApiAdapter.ts b/test/harness/responsesApiAdapter.ts new file mode 100644 index 0000000000..16568f6e03 --- /dev/null +++ b/test/harness/responsesApiAdapter.ts @@ -0,0 +1,437 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { randomUUID } from "node:crypto"; +import type { ChatCompletion } from "openai/resources/chat/completions"; +import type { Response as OpenAIResponse } from "openai/resources/responses/responses"; +import { + CanonicalMessage, + formatSseEvent, + functionToolCalls, + isObject, + JsonObject, +} from "./modelProtocolAdapterShared"; + +export const responsesEndpoint = "/responses"; + +type ResponsesRequest = { + model: string; + instructions?: string; + input?: string | JsonObject[]; + stream?: boolean; + tools?: JsonObject[]; + tool_choice?: unknown; + temperature?: number | null; + top_p?: number | null; + parallel_tool_calls?: boolean | null; +}; + +export type ResponsesApiResponse = OpenAIResponse; + +export function responsesApiRequestToChatCompletion( + requestBody: string, +): string { + const request = JSON.parse(requestBody) as ResponsesRequest; + const messages: CanonicalMessage[] = []; + if (request.instructions) { + messages.push({ role: "system", content: request.instructions }); + } + + if (typeof request.input === "string") { + messages.push({ role: "user", content: request.input }); + } else { + for (const item of request.input ?? []) { + const converted = responseInputItemToCanonicalMessages(item); + messages.push(...converted); + } + } + + return JSON.stringify({ + model: request.model, + messages: coalesceAssistantMessages(messages), + ...(request.tools ? { tools: convertResponsesTools(request.tools) } : {}), + ...(request.tool_choice !== undefined + ? { tool_choice: convertResponsesToolChoice(request.tool_choice) } + : {}), + ...(request.stream !== undefined ? { stream: request.stream } : {}), + ...(request.temperature !== undefined && request.temperature !== null + ? { temperature: request.temperature } + : {}), + ...(request.top_p !== undefined && request.top_p !== null + ? { top_p: request.top_p } + : {}), + ...(request.parallel_tool_calls !== undefined && + request.parallel_tool_calls !== null + ? { parallel_tool_calls: request.parallel_tool_calls } + : {}), + }); +} + +function responseInputItemToCanonicalMessages( + item: JsonObject, +): CanonicalMessage[] { + if (item.type === "function_call") { + const callId = + typeof item.call_id === "string" + ? item.call_id + : typeof item.id === "string" + ? item.id + : ""; + return [ + { + role: "assistant", + content: null, + tool_calls: [ + { + id: callId, + type: "function", + function: { + name: typeof item.name === "string" ? item.name : "", + arguments: + typeof item.arguments === "string" ? item.arguments : "{}", + }, + }, + ], + }, + ]; + } + + if (item.type === "function_call_output") { + return [ + { + role: "tool", + tool_call_id: typeof item.call_id === "string" ? item.call_id : "", + content: + typeof item.output === "string" + ? item.output + : JSON.stringify(item.output ?? ""), + }, + ]; + } + + if (item.type === "reasoning") return []; + + if ( + item.type !== "message" && + item.role !== "user" && + item.role !== "assistant" && + item.role !== "system" + ) { + return []; + } + + const role = + item.role === "assistant" || item.role === "system" ? item.role : "user"; + if (typeof item.content === "string") { + return [{ role, content: item.content }]; + } + if (!Array.isArray(item.content)) return [{ role, content: "" }]; + + const parts: unknown[] = []; + for (const part of item.content) { + if (!isObject(part)) continue; + if ( + (part.type === "input_text" || part.type === "output_text") && + typeof part.text === "string" + ) { + parts.push({ type: "text", text: part.text }); + } else if ( + part.type === "input_image" && + typeof part.image_url === "string" + ) { + parts.push({ + type: "image_url", + image_url: { + url: part.image_url, + ...(typeof part.detail === "string" ? { detail: part.detail } : {}), + }, + }); + } else if ( + part.type === "input_file" && + typeof part.file_data === "string" + ) { + parts.push({ + type: "file", + file: { + file_data: part.file_data, + ...(typeof part.filename === "string" + ? { filename: part.filename } + : {}), + }, + }); + } + } + + const onlyText = parts.every( + (part) => isObject(part) && part.type === "text", + ); + return [ + { + role, + content: onlyText + ? parts + .map((part) => + isObject(part) && typeof part.text === "string" ? part.text : "", + ) + .join("") + : parts, + }, + ]; +} + +function coalesceAssistantMessages( + messages: CanonicalMessage[], +): CanonicalMessage[] { + const result: CanonicalMessage[] = []; + for (const message of messages) { + const previous = result[result.length - 1]; + if (message.role === "assistant" && previous?.role === "assistant") { + const previousText = + typeof previous.content === "string" ? previous.content : ""; + const currentText = + typeof message.content === "string" ? message.content : ""; + previous.content = `${previousText}${currentText}` || null; + const toolCalls = [ + ...(previous.tool_calls ?? []), + ...(message.tool_calls ?? []), + ]; + if (toolCalls.length) previous.tool_calls = toolCalls; + } else { + result.push(message); + } + } + return result; +} + +function convertResponsesTools(tools: JsonObject[]): JsonObject[] { + return tools + .filter((tool) => tool.type === "function" && typeof tool.name === "string") + .map((tool) => ({ + type: "function", + function: { + name: tool.name, + ...(typeof tool.description === "string" + ? { description: tool.description } + : {}), + ...(isObject(tool.parameters) ? { parameters: tool.parameters } : {}), + ...(typeof tool.strict === "boolean" ? { strict: tool.strict } : {}), + }, + })); +} + +function convertResponsesToolChoice(toolChoice: unknown): unknown { + if ( + toolChoice === "auto" || + toolChoice === "none" || + toolChoice === "required" + ) { + return toolChoice; + } + if ( + isObject(toolChoice) && + toolChoice.type === "function" && + typeof toolChoice.name === "string" + ) { + return { + type: "function", + function: { name: toolChoice.name }, + }; + } + return undefined; +} + +export function chatCompletionResponseToResponsesApiMessage( + response: ChatCompletion, +): ResponsesApiResponse { + const output: ResponsesApiResponse["output"] = []; + const outputText: string[] = []; + + for (const choice of response.choices) { + if (choice.message.content) { + const text = choice.message.content; + outputText.push(text); + output.push({ + type: "message", + id: `msg_${randomUUID()}`, + role: "assistant", + status: "completed", + content: [ + { + type: "output_text", + text, + annotations: [], + }, + ], + }); + } + for (const toolCall of functionToolCalls(choice.message)) { + output.push({ + type: "function_call", + id: `fc_${toolCall.id}`, + call_id: toolCall.id, + name: toolCall.function.name, + arguments: toolCall.function.arguments, + status: "completed", + }); + } + } + + const finishReason = response.choices[0]?.finish_reason; + return { + id: response.id, + object: "response", + created_at: response.created, + model: response.model, + status: "completed", + output, + output_text: outputText.join(""), + incomplete_details: + finishReason === "length" + ? { reason: "max_output_tokens" } + : finishReason === "content_filter" + ? { reason: "content_filter" } + : null, + error: null, + instructions: null, + metadata: null, + parallel_tool_calls: false, + temperature: null, + tool_choice: "auto", + tools: [], + top_p: null, + usage: { + input_tokens: response.usage?.prompt_tokens ?? 0, + output_tokens: response.usage?.completion_tokens ?? 0, + total_tokens: response.usage?.total_tokens ?? 0, + input_tokens_details: { + cached_tokens: + response.usage?.prompt_tokens_details?.cached_tokens ?? 0, + }, + output_tokens_details: { + reasoning_tokens: + response.usage?.completion_tokens_details?.reasoning_tokens ?? 0, + }, + }, + }; +} + +export function chatCompletionResponseToResponsesApiSseChunks( + response: ChatCompletion, +): string[] { + const fullResponse = chatCompletionResponseToResponsesApiMessage(response); + const skeleton = { + ...fullResponse, + status: "in_progress" as const, + output: [], + output_text: "", + usage: undefined, + }; + const chunks: string[] = []; + let sequenceNumber = 0; + const event = (type: string, data: JsonObject) => + formatSseEvent(type, { + type, + sequence_number: sequenceNumber++, + ...data, + }); + + chunks.push( + event("response.created", { response: skeleton }), + event("response.in_progress", { response: skeleton }), + ); + + for ( + let outputIndex = 0; + outputIndex < fullResponse.output.length; + outputIndex++ + ) { + const item = fullResponse.output[outputIndex]; + const addedItem = + item.type === "message" + ? { ...item, status: "in_progress" as const, content: [] } + : item.type === "function_call" + ? { ...item, status: "in_progress" as const, arguments: "" } + : item; + chunks.push( + event("response.output_item.added", { + output_index: outputIndex, + item: addedItem, + }), + ); + + if (item.type === "message" && Array.isArray(item.content)) { + for ( + let contentIndex = 0; + contentIndex < item.content.length; + contentIndex++ + ) { + const part = item.content[contentIndex]; + chunks.push( + event("response.content_part.added", { + item_id: item.id, + output_index: outputIndex, + content_index: contentIndex, + part: + isObject(part) && part.type === "output_text" + ? { ...part, text: "" } + : part, + }), + ); + if ( + isObject(part) && + part.type === "output_text" && + typeof part.text === "string" + ) { + chunks.push( + event("response.output_text.delta", { + item_id: item.id, + output_index: outputIndex, + content_index: contentIndex, + delta: part.text, + logprobs: [], + }), + event("response.output_text.done", { + item_id: item.id, + output_index: outputIndex, + content_index: contentIndex, + text: part.text, + logprobs: [], + }), + ); + } + chunks.push( + event("response.content_part.done", { + item_id: item.id, + output_index: outputIndex, + content_index: contentIndex, + part, + }), + ); + } + } else if (item.type === "function_call") { + chunks.push( + event("response.function_call_arguments.delta", { + item_id: item.id, + output_index: outputIndex, + delta: item.arguments, + }), + event("response.function_call_arguments.done", { + item_id: item.id, + output_index: outputIndex, + arguments: item.arguments, + }), + ); + } + + chunks.push( + event("response.output_item.done", { + output_index: outputIndex, + item, + }), + ); + } + + chunks.push(event("response.completed", { response: fullResponse })); + return chunks; +} 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 d3e915f6d4..078ba05483 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 @@ -13,7 +13,7 @@ conversations: function: name: exit_plan_mode arguments: '{"summary":"Greeting file implementation - plan","actions":["interactive","autopilot","exit_only"],"recommendedAction":"interactive"}' + plan","actions":["autopilot","interactive","exit_only"],"recommendedAction":"interactive"}' - role: tool tool_call_id: toolcall_0 content: >- diff --git a/test/snapshots/session/should_abort_a_session.yaml b/test/snapshots/session/should_abort_a_session.yaml index f1217f7f62..dbbbd32aa7 100644 --- a/test/snapshots/session/should_abort_a_session.yaml +++ b/test/snapshots/session/should_abort_a_session.yaml @@ -50,31 +50,3 @@ conversations: content: What is 2+2? - role: assistant content: 2 + 2 = 4 - - messages: - - role: system - content: ${system} - - role: user - content: run the shell command 'sleep 100' (note this works on both bash and PowerShell) - - role: assistant - content: I'll run the sleep command for 100 seconds. - tool_calls: - - id: toolcall_0 - type: function - function: - name: report_intent - arguments: '{"intent":"Running sleep command"}' - - id: toolcall_1 - type: function - function: - name: ${shell} - arguments: '{"command":"sleep 100","description":"Run sleep 100 command","mode":"sync","initial_wait":105}' - - role: tool - tool_call_id: toolcall_0 - content: The execution of this tool, or a previous tool was interrupted. - - role: tool - tool_call_id: toolcall_1 - content: The execution of this tool, or a previous tool was interrupted. - - role: user - content: What is 2+2? - - role: assistant - content: 2 + 2 = 4