diff --git a/.github/workflows/java-publish-snapshot.yml b/.github/workflows/java-publish-snapshot.yml
index ce5b8c5f4e..77d5287964 100644
--- a/.github/workflows/java-publish-snapshot.yml
+++ b/.github/workflows/java-publish-snapshot.yml
@@ -171,6 +171,80 @@ jobs:
if-no-files-found: error
retention-days: 1
+ build-linuxmusl-arm64-classifier:
+ name: Build Linux musl ARM64 snapshot classifier
+ needs: resolve-source
+ runs-on: ubuntu-24.04-arm
+ permissions:
+ contents: read
+ outputs:
+ version: ${{ steps.build.outputs.version }}
+ defaults:
+ run:
+ shell: bash
+ working-directory: ./java
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: ${{ github.sha }}
+ fetch-depth: 1
+ persist-credentials: false
+
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ with:
+ node-version: 22
+
+ - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
+ with:
+ java-version: "25"
+ distribution: "microsoft"
+ cache: "maven"
+
+ - name: Build and validate linuxmusl-arm64 classifier
+ id: build
+ run: |
+ set -euo pipefail
+ SOURCE_COMMIT=$(git rev-parse HEAD)
+ if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.source_sha }}" ]; then
+ echo "::error::Checked out $SOURCE_COMMIT instead of the resolved snapshot source."
+ exit 1
+ fi
+ VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
+ docker run --rm \
+ --volume "$GITHUB_WORKSPACE:/workspace" \
+ --volume "$HOME/.m2:/root/.m2" \
+ --workdir /workspace/java \
+ --env HOST_GID="$(id -g)" \
+ --env HOST_UID="$(id -u)" \
+ "eclipse-temurin:25-jdk-alpine" \
+ sh -c "apk add --no-cache git java-cacerts maven nodejs npm &&
+ export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts &&
+ git config --global --add safe.directory /workspace &&
+ node copilot-native/scripts/validate-native-host.mjs linuxmusl-arm64 &&
+ mvn -B -pl copilot-native package -DskipTests -Dcopilot.native.libc=musl &&
+ chown -R \$HOST_UID:\$HOST_GID copilot-native/target"
+ JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-linuxmusl-arm64.jar"
+ PRIMARY_JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION.jar"
+ test -f "$JAR"
+ node copilot-native/scripts/validate-native-artifact.mjs \
+ classifier linuxmusl-arm64 "$JAR" "$(basename "$JAR")" ..
+ node copilot-native/scripts/validate-native-artifact.mjs placeholder "$PRIMARY_JAR"
+ MANIFEST="copilot-native/target/linuxmusl-arm64-$VERSION.sha256"
+ HASH=$(sha256sum "$JAR" | cut -d ' ' -f 1)
+ printf '%s %s' "$HASH" "$(basename "$JAR")" > "$MANIFEST"
+ node copilot-native/scripts/validate-native-artifact.mjs \
+ checksum "$JAR" "$MANIFEST" "$(basename "$JAR")"
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: java-native-linuxmusl-arm64-snapshot-${{ github.run_id }}-${{ github.run_attempt }}
+ path: |
+ java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-linuxmusl-arm64.jar
+ java/copilot-native/target/linuxmusl-arm64-${{ steps.build.outputs.version }}.sha256
+ if-no-files-found: error
+ retention-days: 1
+
build-windows-x64-classifier:
name: Build Windows x64 snapshot classifier
needs: resolve-source
@@ -426,6 +500,7 @@ jobs:
resolve-source,
build-linux-arm64-classifier,
build-linuxmusl-x64-classifier,
+ build-linuxmusl-arm64-classifier,
build-windows-x64-classifier,
build-windows-arm64-classifier,
build-darwin-classifier,
@@ -468,6 +543,11 @@ jobs:
name: java-native-linuxmusl-x64-snapshot-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/java-native-linuxmusl-x64
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: java-native-linuxmusl-arm64-snapshot-${{ github.run_id }}-${{ github.run_attempt }}
+ path: ${{ runner.temp }}/java-native-linuxmusl-arm64
+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: java-native-win32-x64-snapshot-${{ github.run_id }}-${{ github.run_attempt }}
@@ -569,6 +649,31 @@ jobs:
echo "linuxmusl_x64_jar=$JAR" >> "$GITHUB_OUTPUT"
echo "linuxmusl_x64_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT"
+ - name: Verify version, source, and Linux musl ARM64 classifier
+ id: linuxmusl-arm64-artifact
+ run: |
+ SOURCE_COMMIT=$(git rev-parse HEAD)
+ if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.source_sha }}" ]; then
+ echo "::error::Checked out $SOURCE_COMMIT instead of the resolved snapshot source."
+ exit 1
+ fi
+ VERSION="${{ steps.linux-arm64-artifact.outputs.version }}"
+ if [ "$VERSION" != "${{ needs.build-linuxmusl-arm64-classifier.outputs.version }}" ]; then
+ echo "::error::Linux musl ARM64 classifier version does not match deploy version."
+ exit 1
+ fi
+ ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-linuxmusl-arm64"
+ JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-linuxmusl-arm64.jar"
+ MANIFEST="$ARTIFACT_DIRECTORY/linuxmusl-arm64-$VERSION.sha256"
+ test -f "$JAR"
+ test -f "$MANIFEST"
+ node "$GITHUB_WORKSPACE/java/copilot-native/scripts/validate-native-artifact.mjs" \
+ checksum "$JAR" "$MANIFEST" "$(basename "$JAR")"
+ node "$GITHUB_WORKSPACE/java/copilot-native/scripts/validate-native-artifact.mjs" \
+ classifier linuxmusl-arm64 "$JAR" "$(basename "$JAR")" "$GITHUB_WORKSPACE"
+ echo "linuxmusl_arm64_jar=$JAR" >> "$GITHUB_OUTPUT"
+ echo "linuxmusl_arm64_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT"
+
- name: Verify version, source, and Darwin classifier
id: darwin-artifact
run: |
@@ -650,6 +755,7 @@ jobs:
mvn -B deploy -DskipTests -DskipITs -Dcopilot.native.libc=glibc \
"-Dcopilot.native.external.linux.arm64.classifier.path=${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}" \
"-Dcopilot.native.external.linuxmusl.x64.classifier.path=${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_jar }}" \
+ "-Dcopilot.native.external.linuxmusl.arm64.classifier.path=${{ steps.linuxmusl-arm64-artifact.outputs.linuxmusl_arm64_jar }}" \
"-Dcopilot.native.external.win32.classifier.path=${{ steps.windows-artifact.outputs.windows_jar }}" \
"-Dcopilot.native.external.win32.arm64.classifier.path=${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}" \
"-Dcopilot.native.external.darwin.classifier.path=${{ steps.darwin-artifact.outputs.darwin_jar }}" \
@@ -689,6 +795,7 @@ jobs:
echo "| \`linux-x64\` | \`ubuntu-latest\` | \`$(basename "$LINUX_JAR")\` | \`$LINUX_SHA\` | Published |"
echo "| \`linux-arm64\` | \`ubuntu-24.04-arm\` | \`$(basename "${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}")\` | \`${{ steps.linux-arm64-artifact.outputs.linux_arm64_sha }}\` | Published |"
echo "| \`linuxmusl-x64\` | \`Alpine x64\` | \`$(basename "${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_jar }}")\` | \`${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_sha }}\` | Published |"
+ echo "| \`linuxmusl-arm64\` | \`Alpine ARM64\` | \`$(basename "${{ steps.linuxmusl-arm64-artifact.outputs.linuxmusl_arm64_jar }}")\` | \`${{ steps.linuxmusl-arm64-artifact.outputs.linuxmusl_arm64_sha }}\` | Published |"
echo "| \`win32-x64\` | \`windows-latest\` | \`$(basename "${{ steps.windows-artifact.outputs.windows_jar }}")\` | \`${{ steps.windows-artifact.outputs.windows_sha }}\` | Published |"
echo "| \`win32-arm64\` | \`windows-11-arm\` | \`$(basename "${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}")\` | \`${{ steps.windows-arm64-artifact.outputs.windows_arm64_sha }}\` | Published |"
echo "| \`darwin-x64\` | \`macos-15-intel\` | \`$(basename "${{ steps.darwin-x64-artifact.outputs.darwin_x64_jar }}")\` | \`${{ steps.darwin-x64-artifact.outputs.darwin_x64_sha }}\` | Published |"
diff --git a/.github/workflows/java-smoke-test.yml b/.github/workflows/java-smoke-test.yml
deleted file mode 100644
index 95f662beb1..0000000000
--- a/.github/workflows/java-smoke-test.yml
+++ /dev/null
@@ -1,125 +0,0 @@
-name: "Java smoke test"
-
-on:
- workflow_dispatch:
- workflow_call:
- secrets:
- COPILOT_GITHUB_TOKEN:
- required: true
-
-permissions:
- contents: read
-
-jobs:
- smoke-test-jdk17:
- name: Build SDK and run smoke test (JDK 17)
- runs-on: ubuntu-latest
- if: github.ref == 'refs/heads/main'
- defaults:
- run:
- shell: bash
- working-directory: ./java
- steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
-
- - name: Set up JDK 17
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
- with:
- java-version: "17"
- distribution: "microsoft"
- cache: "maven"
-
- - uses: ./.github/actions/setup-copilot
- id: setup-copilot
-
- - name: Build SDK and install to local repo
- run: mvn -DskipTests -DskipITs -Pskip-test-harness clean install
-
- - name: Create and run smoke test via Copilot CLI
- env:
- COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- COPILOT_CLI_JS: ${{ steps.setup-copilot.outputs.javascript-cli-path }}
- run: |
- cat > /tmp/smoke-test-prompt.txt << 'PROMPT_EOF'
- You are running inside the copilot-sdk monorepo, in the java/ subdirectory.
- The SDK has already been built and installed into the local Maven repository.
- JDK 17 and Maven are already installed and on PATH.
-
- Execute the prompt at `sdk/src/test/prompts/PROMPT-smoke-test.md` with the following critical overrides:
-
- **Critical override — disable SNAPSHOT updates (but allow downloads):** The goal of this workflow is to validate the SDK SNAPSHOT that was just built and installed locally, not any newer SNAPSHOT that might exist in a remote repository. To ensure Maven does not download a newer timestamped SNAPSHOT of the SDK while still allowing it to download any missing plugins or dependencies, you must run the smoke-test Maven build without `-U` and with `--no-snapshot-updates`, so that it uses the locally installed SDK artifact. Use `mvn --no-snapshot-updates clean package` instead of `mvn -U clean package` or `mvn -o clean package`.
-
- **Critical override — do NOT run the jar:** Stop after the `mvn --no-snapshot-updates clean package` build succeeds. Do NOT execute Step 4 (java -jar) or Step 5 (verify exit code) from the prompt. The workflow will run the jar in a separate deterministic step to guarantee the exit code propagates correctly.
-
- Follow steps 1-3 only: create the `smoke-test/` directory, create `pom.xml` and the Java source file exactly as specified, and build with `mvn --no-snapshot-updates clean package` (no SNAPSHOT updates and without `-U`).
-
- If any step fails, exit with a non-zero exit code. Do not silently fix errors.
- PROMPT_EOF
-
- node "$COPILOT_CLI_JS" --yolo --prompt "$(cat /tmp/smoke-test-prompt.txt)"
-
- - name: Run smoke test jar
- env:
- COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- run: |
- cd smoke-test
- java -jar ./target/copilot-sdk-smoketest-1.0-SNAPSHOT.jar
- echo "Smoke test passed (exit code 0)"
-
- smoke-test-java25:
- name: Build SDK and run smoke test (JDK 25)
- runs-on: ubuntu-latest
- if: github.ref == 'refs/heads/main'
- defaults:
- run:
- shell: bash
- working-directory: ./java
- steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
-
- - name: Set up JDK 25
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
- with:
- java-version: "25"
- distribution: "microsoft"
- cache: "maven"
-
- - uses: ./.github/actions/setup-copilot
- id: setup-copilot
-
- - name: Build SDK and install to local repo
- run: mvn -DskipTests -DskipITs -Pskip-test-harness clean install
-
- - name: Create and run smoke test via Copilot CLI
- env:
- COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- COPILOT_CLI_JS: ${{ steps.setup-copilot.outputs.javascript-cli-path }}
- run: |
- cat > /tmp/smoke-test-prompt.txt << 'PROMPT_EOF'
- You are running inside the copilot-sdk monorepo, in the java/ subdirectory.
- The SDK has already been built and installed into the local Maven repository.
- JDK 25 and Maven are already installed and on PATH.
-
- Execute the prompt at `sdk/src/test/prompts/PROMPT-smoke-test.md` with the following critical overrides:
-
- **Critical override — disable SNAPSHOT updates (but allow downloads):** The goal of this workflow is to validate the SDK SNAPSHOT that was just built and installed locally, not any newer SNAPSHOT that might exist in a remote repository. To ensure Maven does not download a newer timestamped SNAPSHOT of the SDK while still allowing it to download any missing plugins or dependencies, you must run the smoke-test Maven build without `-U` and with `--no-snapshot-updates`, so that it uses the locally installed SDK artifact. Use `mvn --no-snapshot-updates clean package` instead of `mvn -U clean package` or `mvn -o clean package`.
-
- **Critical override — do NOT run the jar:** Stop after the `mvn --no-snapshot-updates clean package` build succeeds. Do NOT execute Step 4 (java -jar) or Step 5 (verify exit code) from the prompt. The workflow will run the jar in a separate deterministic step to guarantee the exit code propagates correctly.
-
- **Critical override — enable Virtual Threads for JDK 25:** After creating the Java source file from the README "Quick Start" section but BEFORE building, you must modify the source file to enable virtual thread support. The Quick Start code contains inline comments that start with `// JDK 25+:` — these are instructions. Find every such comment and follow what it says (comment out lines it says to comment out, uncomment lines it says to uncomment). Add any imports required by the newly uncommented code (e.g. `java.util.concurrent.Executors`).
- Also set `maven.compiler.source` and `maven.compiler.target` to `25` in the `pom.xml`.
-
- Follow steps 1-3 only: create the `smoke-test/` directory, create `pom.xml` and the Java source file exactly as specified, apply the JDK 25 virtual thread modifications described above, and build with `mvn --no-snapshot-updates clean package` (no SNAPSHOT updates and without `-U`).
-
- If any step fails, exit with a non-zero exit code. Do not silently fix errors.
- PROMPT_EOF
-
- node "$COPILOT_CLI_JS" --yolo --prompt "$(cat /tmp/smoke-test-prompt.txt)"
-
- - name: Run smoke test jar
- env:
- COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- run: |
- cd smoke-test
- java -jar ./target/copilot-sdk-smoketest-1.0-SNAPSHOT.jar
- echo "Smoke test passed (exit code 0)"
diff --git a/.github/workflows/sdk-dotnet.yml b/.github/workflows/sdk-dotnet.yml
index 4e9671c9a9..63b689f94e 100644
--- a/.github/workflows/sdk-dotnet.yml
+++ b/.github/workflows/sdk-dotnet.yml
@@ -1,6 +1,6 @@
# Invoked by sdk.yml to run the .NET SDK build/test matrix, documentation,
# full subprocess CAPI coverage, and focused in-process smoke
-# coverage on Linux and x64 musl.
+# coverage across standard platforms and x64 musl.
name: "SDK .NET"
env:
@@ -83,6 +83,13 @@ jobs:
DOTNET_TEST_RESULTS_DIRECTORY: TestResults/inprocess
DOTNET_TEST_FILTER: "FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication"
run: ../scripts/ci/run-dotnet-tests.sh
+ - name: Validate documentation examples
+ if: github.event_name != 'merge_group'
+ working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation
+ run: |
+ npm ci
+ npm run extract
+ npm run validate:cs
- if: failure()
uses: actions/upload-artifact@v7
with:
@@ -92,9 +99,9 @@ jobs:
retention-days: 7
dotnet-darwin-arm64:
- name: ".NET (macos-latest, default, CAPI)"
+ name: ".NET (macos-26-xlarge, default, CAPI)"
if: inputs.platform == 'all' || inputs.platform == 'darwin-arm64'
- runs-on: macos-latest
+ runs-on: macos-26-xlarge
timeout-minutes: 30
defaults:
run:
@@ -102,6 +109,7 @@ jobs:
working-directory: ${{ inputs.sdk-home }}/dotnet
env:
COPILOT_SDK_E2E_BACKEND: capi
+ DOTNET_TEST_RESULTS_DIRECTORY: TestResults/subprocess
steps:
- uses: actions/checkout@v7
timeout-minutes: 4
@@ -123,13 +131,23 @@ jobs:
- run: dotnet build --no-restore -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" -p:CopilotSkipCliDownload=true
- run: npm ci --ignore-scripts
working-directory: ${{ inputs.sdk-home }}/test/harness
- - env:
+ - name: Test out-of-process
+ id: subprocess
+ env:
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
run: ../scripts/ci/run-dotnet-tests.sh
+ - name: Test in-process smoke
+ if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }}
+ env:
+ COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
+ COPILOT_SDK_DEFAULT_CONNECTION: inprocess
+ DOTNET_TEST_RESULTS_DIRECTORY: TestResults/inprocess
+ DOTNET_TEST_FILTER: "FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication"
+ run: ../scripts/ci/run-dotnet-tests.sh
- if: failure()
uses: actions/upload-artifact@v7
with:
- name: dotnet-test-diagnostics-macos-latest-default-capi-${{ github.run_attempt }}
+ name: dotnet-test-diagnostics-macos-26-xlarge-default-capi-${{ github.run_attempt }}
path: ${{ inputs.sdk-home }}/dotnet/TestResults/
if-no-files-found: warn
retention-days: 7
@@ -146,6 +164,7 @@ jobs:
env:
COPILOT_SDK_E2E_BACKEND: capi
DOTNET_TEST_RUNTIME: win-x64
+ DOTNET_TEST_RESULTS_DIRECTORY: TestResults/subprocess
steps:
- uses: actions/checkout@v7
timeout-minutes: 4
@@ -168,9 +187,19 @@ jobs:
- run: npm ci --ignore-scripts
working-directory: ${{ inputs.sdk-home }}/test/harness
- run: pwsh.exe -Command "Write-Host 'PowerShell ready'"
- - env:
+ - name: Test out-of-process
+ id: subprocess
+ env:
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
run: ../scripts/ci/run-dotnet-tests.sh
+ - name: Test in-process smoke
+ if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }}
+ env:
+ COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
+ COPILOT_SDK_DEFAULT_CONNECTION: inprocess
+ DOTNET_TEST_RESULTS_DIRECTORY: TestResults/inprocess
+ DOTNET_TEST_FILTER: "FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication"
+ run: ../scripts/ci/run-dotnet-tests.sh
- if: failure()
uses: actions/upload-artifact@v7
with:
@@ -257,25 +286,3 @@ jobs:
path: ${{ inputs.sdk-home }}/dotnet/TestResults/
if-no-files-found: warn
retention-days: 7
-
- docs-dotnet:
- name: "Docs (.NET)"
- if: inputs.platform == 'all' || inputs.platform == 'docs'
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v7
- timeout-minutes: 4
- with:
- persist-credentials: false
- - uses: actions/setup-node@v6
- with:
- node-version: 22
- - uses: actions/setup-dotnet@v6
- with:
- dotnet-version: "10.0.x"
- - run: npm ci
- working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation
- - run: dotnet restore -p:CopilotSkipCliDownload=true
- working-directory: ${{ inputs.sdk-home }}/dotnet
- - run: npm run extract && npm run validate:cs
- working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation
diff --git a/.github/workflows/sdk-go.yml b/.github/workflows/sdk-go.yml
index 24ef16a35b..e7235998f8 100644
--- a/.github/workflows/sdk-go.yml
+++ b/.github/workflows/sdk-go.yml
@@ -30,7 +30,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }}
+ os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26","windows-latest"]') }}
runs-on: ${{ matrix.os }}
defaults:
run:
@@ -86,6 +86,17 @@ jobs:
run: |
go test -v -race -timeout=20m ./internal/e2e -run '^TestInProcessFfiE2E$'
go test -v -race -timeout=20m ./internal/e2e -run '^TestClientE2E$/^should_use_configured_github_host_for_authentication$'
+ - if: github.event_name != 'merge_group' && runner.os == 'Linux'
+ uses: actions/setup-node@v6
+ with:
+ node-version: 22
+ - name: Validate documentation examples
+ if: github.event_name != 'merge_group' && runner.os == 'Linux'
+ working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation
+ run: |
+ npm ci
+ npm run extract
+ npm run validate:go
go-musl-x64:
name: "Go (Alpine x64)"
@@ -138,24 +149,3 @@ jobs:
go test -v -race -timeout=20m ./internal/e2e -run '^TestInProcessFfiE2E$' || status=$?
go test -v -race -timeout=20m ./internal/e2e -run '^TestClientE2E$/^should_use_configured_github_host_for_authentication$' || status=$?
exit "$status"
-
- docs-go:
- name: "Docs (Go)"
- if: inputs.platform == 'all' || inputs.platform == 'docs'
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v7
- timeout-minutes: 4
- with:
- persist-credentials: false
- - uses: actions/setup-node@v6
- with:
- node-version: 22
- - uses: actions/setup-go@v6
- with:
- go-version: "1.24"
- cache-dependency-path: ${{ inputs.sdk-home }}/go/go.sum
- - run: npm ci
- working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation
- - run: npm run extract && npm run validate:go
- working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation
diff --git a/.github/workflows/sdk-java.yml b/.github/workflows/sdk-java.yml
index 408298d88c..f562cdbb08 100644
--- a/.github/workflows/sdk-java.yml
+++ b/.github/workflows/sdk-java.yml
@@ -1,5 +1,5 @@
-# Invoked by sdk.yml to run full subprocess Java SDK coverage on supported JDKs,
-# focused in-process smoke coverage across native classifiers, and documentation.
+# Invoked by sdk.yml to run full subprocess Java SDK coverage followed by focused
+# in-process smoke on standard platforms and x64 musl, plus JDK 17 and docs checks.
name: "SDK Java"
env:
@@ -24,13 +24,19 @@ permissions:
jobs:
java:
- name: "Java (JDK ${{ matrix.test-jdk }})"
- if: inputs.platform == 'all' || inputs.platform == 'linux-x64'
+ name: "Java (${{ matrix.os }}, JDK ${{ matrix.test-jdk }})"
+ if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform)
strategy:
fail-fast: false
matrix:
+ os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26","windows-latest"]') }}
test-jdk: ["25", "17"]
- runs-on: ubuntu-latest
+ exclude:
+ - os: macos-26
+ test-jdk: "17"
+ - os: windows-latest
+ test-jdk: "17"
+ runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
@@ -42,7 +48,7 @@ jobs:
persist-credentials: false
- uses: ./.github/actions/download-sdk-runtime
with:
- artifact-name: executable-sdk-Linux-X64-gnu
+ artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }}
path: ${{ github.workspace }}
- run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare
working-directory: .
@@ -56,13 +62,15 @@ jobs:
node-version: 22
# TEMPORARY MERGE QUEUE REDUCTION: pull requests and main pushes retain
# static checks; merge groups rerun only SDK compatibility coverage.
- - if: github.event_name != 'merge_group' && matrix.test-jdk == '25'
+ - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25'
run: ./scripts/test-update-documentation-versions.sh
- run: ./mvnw test-compile jar:jar
- - if: github.event_name != 'merge_group' && matrix.test-jdk == '25'
+ - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25'
run: ./mvnw javadoc:javadoc
- - if: github.event_name != 'merge_group' && matrix.test-jdk == '25'
+ - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25'
run: ./mvnw spotless:check
+ - if: runner.os == 'Windows'
+ run: pwsh.exe -Command "Write-Host 'PowerShell ready'"
- if: matrix.test-jdk == '25'
id: subprocess
env:
@@ -94,10 +102,19 @@ jobs:
-Dsurefire.failIfNoSpecifiedTests=false \
-Dit.test=InProcessTransportIT,AuthHostE2ETest \
-Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH"
+ - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25'
+ run: ./mvnw install -Dmaven.test.skip=true -Dskip.test.harness=true -Dcopilot.native.skip.download=true
+ - name: Validate documentation examples
+ if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25'
+ working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation
+ run: |
+ npm ci
+ npm run extract
+ npm run validate:java
- if: failure()
uses: actions/upload-artifact@v7
with:
- name: java-test-results-jdk-${{ matrix.test-jdk }}
+ name: java-test-results-${{ matrix.os }}-jdk-${{ matrix.test-jdk }}
path: |
${{ inputs.sdk-home }}/java/sdk/target/surefire-reports/
${{ inputs.sdk-home }}/java/sdk/target/surefire-reports-isolated/
@@ -105,18 +122,11 @@ jobs:
${{ inputs.sdk-home }}/java/sdk/target/subprocess-reports/
retention-days: 7
- java-inprocess:
- name: "Java (inprocess, ${{ matrix.classifier }})"
- if: contains(fromJSON('["all","darwin-arm64","win32-x64"]'), inputs.platform)
- strategy:
- fail-fast: false
- matrix:
- include: ${{ fromJSON(inputs.platform == 'darwin-arm64' && '[{"os":"macos-26","classifier":"darwin-arm64"}]' || inputs.platform == 'win32-x64' && '[{"os":"windows-latest","classifier":"win32-x64"}]' || '[{"os":"windows-latest","classifier":"win32-x64"},{"os":"macos-26","classifier":"darwin-arm64"}]') }}
- runs-on: ${{ matrix.os }}
- defaults:
- run:
- shell: bash
- working-directory: ${{ inputs.sdk-home }}/java
+ java-musl-x64:
+ name: "Java (JDK 25, linuxmusl-x64)"
+ if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64'
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
steps:
- uses: actions/checkout@v7
timeout-minutes: 4
@@ -124,40 +134,59 @@ jobs:
persist-credentials: false
- uses: ./.github/actions/download-sdk-runtime
with:
- artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }}
+ artifact-name: executable-sdk-Linux-X64-musl
path: ${{ github.workspace }}
- - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare
+ - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore
working-directory: .
- - uses: actions/setup-java@v5
- with:
- java-version: "25"
- distribution: microsoft
- cache: maven
- - uses: actions/setup-node@v6
+ - uses: ./.github/actions/run-alpine-tests
+ env:
+ COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
with:
- node-version: 22
- - env:
- CI: "true"
- run: >-
- ./mvnw clean verify -Pinprocess
- -Dtest=NoTestsForInProcessSmoke
- -Dsurefire.failIfNoSpecifiedTests=false
- -Dit.test=InProcessTransportIT,AuthHostE2ETest
- -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH"
+ image: eclipse-temurin:25-jdk-alpine
+ sdk-root: /workspace/${{ inputs.sdk-home }}
+ workdir: /workspace/${{ inputs.sdk-home }}/java
+ command: |
+ apk add --no-cache java-cacerts maven nodejs npm
+ export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts
+ export GITHUB_ENV=/tmp/copilot-sdk-runtime.env
+ export COPILOT_RUNTIME_TARGET=linuxmusl-x64
+ export COPILOT_RUNTIME_OUTPUT_DIRECTORY=/workspace/.sdk-runtime/linuxmusl-x64
+ node "$COPILOT_SDK_ROOT/scripts/ci/runtime-artifact.mjs" prepare
+ set -a
+ . "$GITHUB_ENV"
+ set +a
+ node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64
+ ./mvnw test-compile jar:jar -Dcopilot.native.libc=musl
+ status=0
+ ./mvnw -pl sdk verify -Dskip.test.harness=true -Dcopilot.cli.path="$COPILOT_CLI_PATH" || status=$?
+ mkdir -p sdk/target/subprocess-reports
+ for reports in surefire-reports surefire-reports-isolated failsafe-reports; do
+ if [ -d "sdk/target/$reports" ]; then
+ mv "sdk/target/$reports" sdk/target/subprocess-reports/
+ fi
+ done
+ ./mvnw verify -Pinprocess \
+ -Dskip.test.harness=true \
+ -Dtest=NoTestsForInProcessSmoke \
+ -Dsurefire.failIfNoSpecifiedTests=false \
+ -Dit.test=InProcessTransportIT,AuthHostE2ETest \
+ -Dcopilot.native.libc=musl \
+ -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" || status=$?
+ exit "$status"
- if: failure()
uses: actions/upload-artifact@v7
with:
- name: java-test-results-inprocess-${{ matrix.classifier }}
+ name: java-test-results-linuxmusl-x64
path: |
${{ inputs.sdk-home }}/java/sdk/target/surefire-reports/
${{ inputs.sdk-home }}/java/sdk/target/surefire-reports-isolated/
${{ inputs.sdk-home }}/java/sdk/target/failsafe-reports/
+ ${{ inputs.sdk-home }}/java/sdk/target/subprocess-reports/
retention-days: 7
-
- java-inprocess-musl-x64:
- name: "Java (inprocess, linuxmusl-x64)"
- if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64'
- runs-on: ubuntu-latest
+ java-musl-arm64:
+ name: "Java (JDK 25, linuxmusl-arm64)"
+ if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-arm64'
+ runs-on: ubuntu-24.04-arm
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
@@ -166,11 +195,11 @@ jobs:
persist-credentials: false
- uses: ./.github/actions/download-sdk-runtime
with:
- artifact-name: executable-sdk-Linux-X64-musl
+ artifact-name: executable-sdk-Linux-ARM64-musl
path: ${{ github.workspace }}
- run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore
working-directory: .
- - name: Run Java SDK tests (InProcess, musl)
+ - name: Run Java SDK tests (linuxmusl-arm64)
env:
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
run: |
@@ -181,56 +210,44 @@ jobs:
--env COPILOT_HMAC_KEY \
--env GITHUB_ACTIONS=true \
--env GITHUB_WORKSPACE=/workspace \
- --env SDK_HOME="/workspace/$SDK_HOME" \
+ --env COPILOT_SDK_ROOT="/workspace/$SDK_HOME" \
eclipse-temurin:25-jdk-alpine \
sh -c 'set -eux
- apk add --no-cache git java-cacerts maven nodejs npm
- export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts
+ apk add --no-cache bash git java-cacerts maven nodejs npm
git config --global --add safe.directory /workspace
+ export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts
export GITHUB_ENV=/tmp/copilot-sdk-runtime.env
- export COPILOT_RUNTIME_TARGET=linuxmusl-x64
- export COPILOT_RUNTIME_OUTPUT_DIRECTORY=/workspace/.sdk-runtime/linuxmusl-x64
- node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare
+ export COPILOT_RUNTIME_TARGET=linuxmusl-arm64
+ export COPILOT_RUNTIME_OUTPUT_DIRECTORY=/workspace/.sdk-runtime/linuxmusl-arm64
+ node "$COPILOT_SDK_ROOT/scripts/ci/runtime-artifact.mjs" prepare
set -a
. "$GITHUB_ENV"
set +a
- node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64
- ./mvnw clean verify -Pinprocess \
+ node copilot-native/scripts/validate-native-host.mjs linuxmusl-arm64
+ ./mvnw test-compile jar:jar -Dcopilot.native.libc=musl
+ status=0
+ ./mvnw -pl sdk verify -Dskip.test.harness=true -Dcopilot.cli.path="$COPILOT_CLI_PATH" || status=$?
+ mkdir -p sdk/target/subprocess-reports
+ for reports in surefire-reports surefire-reports-isolated failsafe-reports; do
+ if [ -d "sdk/target/$reports" ]; then
+ mv "sdk/target/$reports" sdk/target/subprocess-reports/
+ fi
+ done
+ ./mvnw verify -Pinprocess \
+ -Dskip.test.harness=true \
-Dtest=NoTestsForInProcessSmoke \
-Dsurefire.failIfNoSpecifiedTests=false \
-Dit.test=InProcessTransportIT,AuthHostE2ETest \
-Dcopilot.native.libc=musl \
- -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH"'
+ -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" || status=$?
+ exit "$status"'
- if: failure()
uses: actions/upload-artifact@v7
with:
- name: java-test-results-inprocess-linuxmusl-x64
+ name: java-test-results-linuxmusl-arm64
path: |
${{ inputs.sdk-home }}/java/sdk/target/surefire-reports/
${{ inputs.sdk-home }}/java/sdk/target/surefire-reports-isolated/
${{ inputs.sdk-home }}/java/sdk/target/failsafe-reports/
+ ${{ inputs.sdk-home }}/java/sdk/target/subprocess-reports/
retention-days: 7
-
- docs-java:
- name: "Docs (Java)"
- if: inputs.platform == 'all' || inputs.platform == 'docs'
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v7
- timeout-minutes: 4
- with:
- persist-credentials: false
- - uses: actions/setup-node@v6
- with:
- node-version: 22
- - uses: actions/setup-java@v5
- with:
- distribution: microsoft
- java-version: "25"
- cache: maven
- - run: ./mvnw install -Dmaven.test.skip=true -Dskip.test.harness=true -Dcopilot.native.skip.download=true
- working-directory: ${{ inputs.sdk-home }}/java
- - run: npm ci
- working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation
- - run: npm run extract && npm run validate:java
- working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation
diff --git a/.github/workflows/sdk-nodejs.yml b/.github/workflows/sdk-nodejs.yml
index 315331609b..9fbbf97e76 100644
--- a/.github/workflows/sdk-nodejs.yml
+++ b/.github/workflows/sdk-nodejs.yml
@@ -19,6 +19,11 @@ on:
sdk-home:
required: true
type: string
+ outputs:
+ capi-result:
+ description: "Standard-platform CAPI job result, independent of BYOK jobs"
+ # Direct result outputs can be empty: https://github.com/actions/runner/issues/2495
+ value: ${{ fromJSON(toJSON(jobs.nodejs)).result }}
permissions:
contents: read
@@ -30,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }}
+ os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26","windows-latest"]') }}
runs-on: ${{ matrix.os }}
defaults:
run:
@@ -89,13 +94,13 @@ jobs:
env:
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
run: |
- if [ "$GITHUB_EVENT_NAME" = "merge_group" ]; then
+ if [ "$GITHUB_EVENT_NAME" = "merge_group" ] || [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
npm test -- --reporter=default --reporter=json --outputFile="$RUNNER_TEMP/sdk-nodejs-results.json"
else
npm test
fi
- - name: Upload merge-queue Node.js test results
- if: always() && github.event_name == 'merge_group' && runner.os == 'Linux'
+ - name: Upload Flake Finder Node.js test results
+ if: always() && (github.event_name == 'merge_group' || github.event_name == 'pull_request') && runner.os == 'Linux'
continue-on-error: true
uses: actions/upload-artifact@v7
with:
@@ -109,6 +114,13 @@ jobs:
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
COPILOT_SDK_DEFAULT_CONNECTION: inprocess
run: npm test -- test/e2e/inprocess_ffi.e2e.test.ts test/e2e/auth_host.e2e.test.ts
+ - name: Validate documentation examples
+ if: github.event_name != 'merge_group' && runner.os == 'Linux'
+ working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation
+ run: |
+ npm ci
+ npm run extract
+ npm run validate:ts
# Keep each BYOK backend on its own runner; shared runtime E2E coverage belongs
# in TypeScript rather than repeating these sweeps across SDK languages.
@@ -197,24 +209,3 @@ jobs:
npm test || status=$?
COPILOT_SDK_DEFAULT_CONNECTION=inprocess npm test -- test/e2e/inprocess_ffi.e2e.test.ts test/e2e/auth_host.e2e.test.ts || status=$?
exit "$status"
-
- docs-typescript:
- name: "Docs (TypeScript)"
- if: inputs.platform == 'all' || inputs.platform == 'docs'
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v7
- timeout-minutes: 4
- with:
- persist-credentials: false
- - uses: actions/setup-node@v6
- with:
- node-version: 22
- cache: npm
- cache-dependency-path: ${{ inputs.sdk-home }}/nodejs/package-lock.json
- - run: npm ci --ignore-scripts
- working-directory: ${{ inputs.sdk-home }}/nodejs
- - run: npm ci
- working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation
- - run: npm run extract && npm run validate:ts
- working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation
diff --git a/.github/workflows/sdk-platform.yml b/.github/workflows/sdk-platform.yml
index b21a9106fc..f1ed4e6ed4 100644
--- a/.github/workflows/sdk-platform.yml
+++ b/.github/workflows/sdk-platform.yml
@@ -16,6 +16,9 @@ on:
description: "Node.js SDK result, independent of other languages"
# Direct result outputs can be empty: https://github.com/actions/runner/issues/2495
value: ${{ fromJSON(toJSON(jobs.nodejs)).result }}
+ nodejs-capi-result:
+ description: "Standard-platform Node.js CAPI result, independent of BYOK and other languages"
+ value: ${{ jobs.nodejs.outputs.capi-result }}
permissions:
contents: read
diff --git a/.github/workflows/sdk-python.yml b/.github/workflows/sdk-python.yml
index 71bedf2eaa..2abeca3d8a 100644
--- a/.github/workflows/sdk-python.yml
+++ b/.github/workflows/sdk-python.yml
@@ -30,7 +30,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }}
+ os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26","windows-latest"]') }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20
defaults:
@@ -83,6 +83,14 @@ jobs:
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
COPILOT_SDK_DEFAULT_CONNECTION: inprocess
run: uv run --locked pytest -v -s e2e/test_inprocess_ffi_e2e.py e2e/test_auth_host_e2e.py
+ - name: Validate documentation examples
+ if: github.event_name != 'merge_group' && runner.os == 'Linux'
+ working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation
+ run: |
+ npm ci
+ npm run extract
+ uv run --locked --python 3.12 --project "$GITHUB_WORKSPACE/$SDK_HOME/python" python3 -m mypy --version
+ uv run --locked --python 3.12 --project "$GITHUB_WORKSPACE/$SDK_HOME/python" npm run validate:py
- name: Upload Python test diagnostics
if: failure()
uses: actions/upload-artifact@v7
@@ -149,29 +157,3 @@ jobs:
include-hidden-files: true
if-no-files-found: warn
retention-days: 7
-
- docs-python:
- name: "Docs (Python)"
- if: inputs.platform == 'all' || inputs.platform == 'docs'
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v7
- timeout-minutes: 4
- with:
- persist-credentials: false
- - uses: actions/setup-node@v6
- with:
- node-version: 22
- - uses: actions/setup-python@v6
- with:
- python-version: "3.12"
- - uses: astral-sh/setup-uv@v7
- - run: uv sync --locked
- working-directory: ${{ inputs.sdk-home }}/python
- - run: npm ci
- working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation
- - run: npm run extract
- working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation
- - run: uv run --locked --project "$GITHUB_WORKSPACE/$SDK_HOME/python" python3 -m mypy --version
- - run: uv run --locked --project "$GITHUB_WORKSPACE/$SDK_HOME/python" npm run validate:py
- working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation
diff --git a/.github/workflows/sdk-rust.yml b/.github/workflows/sdk-rust.yml
index 5ab3134139..abe13e28bb 100644
--- a/.github/workflows/sdk-rust.yml
+++ b/.github/workflows/sdk-rust.yml
@@ -31,7 +31,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }}
+ os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26-xlarge"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26-xlarge","windows-latest"]') }}
runs-on: ${{ matrix.os }}
defaults:
run:
@@ -106,7 +106,7 @@ jobs:
- name: Test in-process smoke
id: inprocess
- if: ${{ !cancelled() && runner.os != 'Windows' && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }}
+ if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }}
env:
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
COPILOT_SDK_DEFAULT_CONNECTION: inprocess
diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml
index b6314e62f6..56c165db89 100644
--- a/.github/workflows/sdk.yml
+++ b/.github/workflows/sdk.yml
@@ -27,8 +27,150 @@ permissions:
contents: read
jobs:
+ detect-sdk-changes:
+ name: "Detect SDK-impacting changes"
+ runs-on: ubuntu-slim
+ outputs:
+ has_sdk_changes: ${{ steps.full-coverage.outputs.has_sdk_changes || steps.classify.outputs.has_sdk_changes || steps.recover.outputs.has_sdk_changes }}
+ steps:
+ - name: Require full coverage outside pull requests
+ id: full-coverage
+ if: github.event_name != 'pull_request'
+ run: echo "has_sdk_changes=true" >> "$GITHUB_OUTPUT"
+
+ - uses: actions/checkout@v7
+ id: checkout
+ if: github.event_name == 'pull_request'
+ continue-on-error: true
+ timeout-minutes: 4
+ with:
+ # Include the synthetic merge commit's base parent without fetching
+ # repository history.
+ fetch-depth: 2
+ persist-credentials: false
+
+ - name: Detect repository layout
+ id: repository-layout
+ if: steps.checkout.outcome == 'success'
+ continue-on-error: true
+ run: |
+ trusted_revision="$(git rev-parse "$GITHUB_SHA^1")"
+ echo "base-sha=$trusted_revision" >> "$GITHUB_OUTPUT"
+ if git cat-file -e "${trusted_revision}:src/sdk/package.json" 2>/dev/null &&
+ git cat-file -e "${trusted_revision}:script/sea-build.ts" 2>/dev/null; then
+ echo "layout=runtime" >> "$GITHUB_OUTPUT"
+ elif git cat-file -e "${trusted_revision}:package.json" 2>/dev/null &&
+ git cat-file -e "${trusted_revision}:nodejs/package.json" 2>/dev/null; then
+ echo "layout=standalone" >> "$GITHUB_OUTPUT"
+ else
+ echo "::warning::Unable to identify the SDK repository layout; requiring full coverage."
+ exit 1
+ fi
+
+ - name: Find SDK-impacting paths
+ id: filter
+ if: steps.repository-layout.outcome == 'success'
+ continue-on-error: true
+ uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
+ with:
+ base_sha: ${{ steps.repository-layout.outputs.base-sha }}
+ sha: ${{ github.sha }}
+ skip_initial_fetch: true
+ fail_on_initial_diff_error: true
+ quotepath: false
+ files_yaml: |
+ runtime:
+ - .github/actions/**
+ - .github/workflows/sdk*.yml
+ - .github/workflows/sdk*.yaml
+ - .github/workflows/shared-cli-build.yml
+ - .github/workflows/publish.yml
+ - .editorconfig
+ - .gitattributes
+ - .nvmrc
+ - package.json
+ - package-lock.json
+ - pnpm-lock.yaml
+ - sdk-protocol-version.json
+ - assets/**
+ - BUILD.bazel
+ - src/**
+ - generated/**
+ - schema/**
+ - files/**
+ - third_party/**
+ - patches/**
+ - tools/bazel/**
+ - .cargo/**
+ - bazel/**
+ - script/**
+ - .bazelignore
+ - .bazelrc
+ - .bazelversion
+ - Cargo.lock
+ - Cargo.toml
+ - MODULE.bazel
+ - MODULE.bazel.lock
+ - pnpm-workspace.yaml
+ - rust-toolchain.toml
+ - esbuild.ts
+ - tsconfig*.json
+ standalone:
+ - .github/actions/**
+ - .github/workflows/sdk*.yml
+ - .github/workflows/sdk*.yaml
+ - .github/workflows/shared-cli-build.yml
+ - .github/workflows/publish.yml
+ - .editorconfig
+ - .gitattributes
+ - .nvmrc
+ - package.json
+ - package-lock.json
+ - pnpm-lock.yaml
+ - sdk-protocol-version.json
+ - assets/**
+ - BUILD.bazel
+ - nodejs/**
+ - python/**
+ - go/**
+ - dotnet/**
+ - java/**
+ - rust/**
+ - scripts/**
+ - samples/**
+ - test/**
+ - docs/**
+ - README.md
+ - vitest.config.ts
+
+ - name: Select layout-specific result
+ id: classify
+ if: steps.filter.outcome == 'success'
+ env:
+ LAYOUT: ${{ steps.repository-layout.outputs.layout }}
+ RUNTIME_CHANGED: ${{ steps.filter.outputs.runtime_any_modified }}
+ STANDALONE_CHANGED: ${{ steps.filter.outputs.standalone_any_modified }}
+ run: |
+ if [[ "$LAYOUT" == "runtime" ]]; then
+ has_sdk_changes="$RUNTIME_CHANGED"
+ elif [[ "$LAYOUT" == "standalone" ]]; then
+ has_sdk_changes="$STANDALONE_CHANGED"
+ else
+ exit 1
+ fi
+ echo "has_sdk_changes=$has_sdk_changes" >> "$GITHUB_OUTPUT"
+
+ - name: Require full coverage after detection errors
+ id: recover
+ if: github.event_name == 'pull_request' && (steps.checkout.outcome != 'success' || steps.repository-layout.outcome != 'success' || steps.filter.outcome != 'success' || steps.classify.outcome != 'success')
+ run: |
+ echo "::warning::SDK change detection failed; requiring full coverage."
+ echo "has_sdk_changes=true" >> "$GITHUB_OUTPUT"
+
detect-layout:
name: "Detect SDK layout"
+ needs: detect-sdk-changes
+ if: ${{ !cancelled() && (needs.detect-sdk-changes.result != 'success' || needs.detect-sdk-changes.outputs.has_sdk_changes == 'true') }}
runs-on: ubuntu-latest
outputs:
runtime-source: ${{ steps.detect.outputs.runtime-source }}
@@ -233,6 +375,26 @@ jobs:
timeout-minutes: 60
secrets: inherit
+ build-runtime-linuxmusl-arm64:
+ name: "Build runtime (linuxmusl-arm64)"
+ needs: detect-layout
+ if: github.event_name != 'merge_group'
+ permissions:
+ actions: read
+ contents: read
+ id-token: write
+ packages: read
+ uses: ./.github/workflows/sdk-runtime-artifact.yml
+ with:
+ runtime-source: ${{ needs.detect-layout.outputs.runtime-source }}
+ sdk-home: ${{ needs.detect-layout.outputs.sdk-home }}
+ os: ubuntu-latest-xl
+ target: linuxmusl-arm64
+ artifact: Linux-ARM64-musl
+ bazel-config: e2e-no-lto
+ timeout-minutes: 60
+ secrets: inherit
+
build-runtime-darwin-arm64:
name: "Build runtime (darwin-arm64)"
needs: detect-layout
@@ -246,7 +408,7 @@ jobs:
with:
runtime-source: ${{ needs.detect-layout.outputs.runtime-source }}
sdk-home: ${{ needs.detect-layout.outputs.sdk-home }}
- os: macos-latest-xlarge
+ os: macos-26-xlarge
target: darwin-arm64
artifact: macOS-ARM64
bazel-config: e2e-no-lto
@@ -296,6 +458,16 @@ jobs:
sdk-home: ${{ needs.detect-layout.outputs.sdk-home }}
secrets: inherit
+ sdk-linuxmusl-arm64:
+ name: "Linux musl ARM64 (Java)"
+ needs: [detect-layout, build-runtime-linuxmusl-arm64]
+ if: github.event_name != 'merge_group'
+ uses: ./.github/workflows/sdk-java.yml
+ with:
+ platform: linuxmusl-arm64
+ sdk-home: ${{ needs.detect-layout.outputs.sdk-home }}
+ secrets: inherit
+
sdk-darwin-arm64:
name: "macOS ARM64"
needs: [detect-layout, build-runtime-darwin-arm64]
@@ -317,40 +489,35 @@ jobs:
sdk-home: ${{ needs.detect-layout.outputs.sdk-home }}
secrets: inherit
- sdk-docs:
- name: "Documentation"
- needs: detect-layout
- if: github.event_name != 'merge_group'
- uses: ./.github/workflows/sdk-platform.yml
- with:
- platform: docs
- sdk-home: ${{ needs.detect-layout.outputs.sdk-home }}
- secrets: inherit
-
+ # Only Linux CAPI gates this rollup; the full SDK aggregate still reports all coverage.
sdk-typescript:
name: sdk-typescript
if: always()
- needs: [sdk-linux-x64, sdk-linuxmusl-x64, sdk-darwin-arm64, sdk-win32-x64]
+ needs: [detect-sdk-changes, sdk-linux-x64]
runs-on: ubuntu-slim
steps:
- - name: Check Node.js SDK results
+ - name: Check Linux CAPI Node.js SDK result
env:
+ DETECT_RESULT: ${{ needs.detect-sdk-changes.result }}
EVENT_NAME: ${{ github.event_name }}
+ HAS_SDK_CHANGES: ${{ needs.detect-sdk-changes.outputs.has_sdk_changes }}
RESULTS: ${{ toJSON(needs) }}
run: |
- for job in sdk-linux-x64 sdk-linuxmusl-x64 sdk-darwin-arm64 sdk-win32-x64; do
- expected=success
- result=$(jq -r --arg job "$job" '.[$job].outputs["nodejs-result"]' <<< "$RESULTS")
- # TEMPORARY MERGE QUEUE REDUCTION: only Linux x64 runs in merge groups.
- if [[ "$EVENT_NAME" == "merge_group" && "$job" != "sdk-linux-x64" ]]; then
- expected=skipped
- result=$(jq -r --arg job "$job" '.[$job].result' <<< "$RESULTS")
- fi
- if [[ "$result" != "$expected" ]]; then
- echo "::error::Node.js SDK job $job: expected $expected, got $result"
+ linux_result=$(jq -r '.["sdk-linux-x64"].result' <<< "$RESULTS")
+ if [[ "$EVENT_NAME" == "pull_request" && "$DETECT_RESULT" == "success" && "$HAS_SDK_CHANGES" == "false" ]]; then
+ if [[ "$linux_result" != "skipped" ]]; then
+ echo "::error::Non-SDK pull request did not skip sdk-linux-x64: $linux_result"
exit 1
fi
- done
+ echo "No SDK-impacting changes; Linux CAPI Node.js SDK job skipped"
+ exit 0
+ fi
+
+ result=$(jq -r '.["sdk-linux-x64"].outputs["nodejs-capi-result"]' <<< "$RESULTS")
+ if [[ "$result" != "success" ]]; then
+ echo "::error::Linux CAPI Node.js SDK job: expected success, got $result"
+ exit 1
+ fi
check-freshness:
name: "Check schema and SDK freshness"
@@ -433,26 +600,45 @@ jobs:
name: "SDK"
if: always()
needs:
+ - detect-sdk-changes
- build-runtime-windows-x64-addons
- build-runtime-linux-x64
- build-runtime-linuxmusl-x64
+ - build-runtime-linuxmusl-arm64
- build-runtime-darwin-arm64
- build-runtime-win32-x64
- detect-layout
- sdk-linux-x64
- sdk-linuxmusl-x64
+ - sdk-linuxmusl-arm64
- sdk-darwin-arm64
- sdk-win32-x64
- - sdk-docs
- check-freshness
runs-on: ubuntu-slim
steps:
- name: Check SDK results
env:
+ DETECT_RESULT: ${{ needs.detect-sdk-changes.result }}
EVENT_NAME: ${{ github.event_name }}
+ HAS_SDK_CHANGES: ${{ needs.detect-sdk-changes.outputs.has_sdk_changes }}
RESULTS: ${{ toJSON(needs) }}
RUNTIME_SOURCE: ${{ needs.detect-layout.outputs.runtime-source }}
run: |
+ if [[ "$EVENT_NAME" == "pull_request" && "$DETECT_RESULT" == "success" && "$HAS_SDK_CHANGES" == "false" ]]; then
+ unexpected=$(jq -r '
+ to_entries[]
+ | select(.key != "detect-sdk-changes" and .value.result != "skipped")
+ | "\(.key): \(.value.result)"
+ ' <<< "$RESULTS")
+ if [[ -n "$unexpected" ]]; then
+ echo "::error::Non-SDK pull request did not skip all SDK jobs:"
+ echo "$unexpected"
+ exit 1
+ fi
+ echo "No SDK-impacting changes; SDK build and test matrix skipped"
+ exit 0
+ fi
+
# TEMPORARY MERGE QUEUE REDUCTION: keep this branch aligned with the
# gated jobs above until the full merge-group matrix is restored.
if [[ "$EVENT_NAME" == "merge_group" ]]; then
@@ -465,12 +651,13 @@ jobs:
skipped_jobs=(
build-runtime-windows-x64-addons
build-runtime-linuxmusl-x64
+ build-runtime-linuxmusl-arm64
build-runtime-darwin-arm64
build-runtime-win32-x64
sdk-linuxmusl-x64
+ sdk-linuxmusl-arm64
sdk-darwin-arm64
sdk-win32-x64
- sdk-docs
)
for job in "${required_jobs[@]}"; do
if [[ "$(jq -r --arg job "$job" '.[$job].result' <<< "$RESULTS")" != "success" ]]; then
@@ -501,6 +688,7 @@ jobs:
failures=$(jq -r --arg runtime_source "$RUNTIME_SOURCE" '
to_entries[]
| select(.value.result != "success")
+ | select(.key != "detect-sdk-changes")
| select(
$runtime_source != "published"
or .key != "build-runtime-windows-x64-addons"
diff --git a/BUILD.bazel b/BUILD.bazel
index e25b8ef8e3..4fa953fbf5 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -4,6 +4,7 @@ SHARED_CODEGEN_INPUTS = [
"scripts/codegen/legacy-parameters.ts",
"scripts/codegen/package-lock.json",
"scripts/codegen/package.json",
+ "scripts/codegen/extensible-enums.ts",
"scripts/codegen/utils.ts",
"scripts/runtime-layout.mjs",
"scripts/runtime-release.mjs",
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9e64a0bb04..93cd587a4c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -316,7 +316,12 @@ additional feature/acquisition choices documented in [rust/AGENTS.md](rust/AGENT
### Documentation checks
-API snippet validation is separate from SDK tests. From the SDK root:
+[SDK CI](.github/workflows/sdk.yml) validates Node.js, Python, Go, .NET, and Java
+API snippets after successful tests in their standard Linux jobs (JDK 25 for Java).
+These checks run on pull requests, pushes to `main`, and manual workflow runs,
+but not merge groups. A documentation failure fails the corresponding language job.
+
+To validate snippets without running SDK tests, run these commands from the SDK root:
```bash
npm --prefix scripts/docs-validation ci
@@ -327,8 +332,15 @@ uv run --locked --project python npm run docs:python
```
This extracts and validates snippets for Node.js, Python, Go, .NET, or Java.
-Java's current docs validator calls `mvn` directly,
-so this task also needs Maven 3.9+ on PATH. There is no `docs:rust` facade;
+`docs:java` installs the SDK once through its Maven wrapper, with tests, replay
+harness setup, and native downloads disabled, then compiles the snippets using
+the same wrapper. CI supplies that installation in its existing post-test step;
+the validator does not reinstall it. A separate Maven installation is not required.
+Java validation also compiles the exact [README Quick Start](java/README.md#quick-start).
+The regular Maven integration suite checks a standalone consumer JAR with a manifest
+classpath and runtime dependencies, without a live model, credentials, or native runtime.
+These checks replace the former agent-driven Java smoke workflow.
+There is no `docs:rust` facade;
follow [the Rust SDK workflow](.github/workflows/sdk-rust.yml) for rustdoc.
### Recording and replaying SDK tests
@@ -344,12 +356,18 @@ For TypeScript SDK E2Es, use the existing
`nodejs/test/e2e/harness/sdkTestContext.ts` fixture. In the runtime repository,
also follow the `e2e-test-author` skill's SDK section.
-SDK CI runs full subprocess coverage followed by a short in-process smoke
-step on the same runner wherever both modes share a platform. Smoke still
-runs if the subprocess tests fail, and either failure fails the job. Java's
-macOS, Windows, and musl smoke jobs remain separate because they have no
-matching subprocess job. Merge groups retain the reduced Linux TypeScript
-CAPI subprocess coverage.
+All six SDKs run full subprocess coverage followed by a short in-process
+smoke step on the same runner on Linux/glibc x64, macOS ARM64, Windows x64,
+and Linux/musl x64. Smoke still runs if the subprocess tests fail, and either
+failure fails the job. Java uses JDK 25 on all four platforms, plus a
+Linux/glibc JDK 17 compatibility job using precompiled classes.
+Merge groups retain the reduced Linux TypeScript CAPI subprocess coverage.
+
+The `sdk-typescript` required rollup checks only the Linux CAPI job, including
+its build, packaging, and applicable static checks. Other platforms, BYOK
+backends, and languages keep their existing scheduling and failure reporting;
+they do not gate this rollup. The full `SDK` aggregate still requires all
+scheduled coverage to succeed.
The three BYOK backend sweeps run in separate Linux TypeScript jobs, alongside
the normal CAPI job; they do not repeat unit tests, packaging, or static
diff --git a/docs/developer-docs/secrets.md b/docs/developer-docs/secrets.md
index 1218f96254..b09ddd213c 100644
--- a/docs/developer-docs/secrets.md
+++ b/docs/developer-docs/secrets.md
@@ -17,7 +17,7 @@ These secrets are used by the authoritative SDK build/test workflow.
These secrets power the GitHub Agentic Workflows (gh-aw) used for issue triage, code generation, and release automation.
* **`COPILOT_GITHUB_TOKEN`**: GitHub OAuth token consumed by the Copilot CLI for AI authentication. Required by all agentic workflows when invoking `copilot` for AI inference.
- * Workflows: `issue-triage.lock.yml`, `issue-classification.lock.yml`, `handle-bug.lock.yml`, `handle-enhancement.lock.yml`, `handle-question.lock.yml`, `handle-documentation.lock.yml`, `java-codegen-check.yml`, `java-codegen-fix.lock.yml`, `java-smoke-test.yml`, `java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml`, `release-changelog.lock.yml`, `cross-repo-issue-analysis.lock.yml`
+ * Workflows: `issue-triage.lock.yml`, `issue-classification.lock.yml`, `handle-bug.lock.yml`, `handle-enhancement.lock.yml`, `handle-question.lock.yml`, `handle-documentation.lock.yml`, `java-codegen-check.yml`, `java-codegen-fix.lock.yml`, `java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml`, `release-changelog.lock.yml`, `cross-repo-issue-analysis.lock.yml`
* **`GH_AW_GITHUB_TOKEN`**: Optional GitHub token override for repository operations (reading code, creating pull requests, and making GitHub API calls). If unset, workflows use the automatic `GITHUB_TOKEN`.
* Workflows: `issue-triage.lock.yml`, `issue-classification.lock.yml`, `handle-bug.lock.yml`, `handle-enhancement.lock.yml`, `handle-question.lock.yml`, `handle-documentation.lock.yml`, `java-codegen-fix.lock.yml`, `java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml`, `release-changelog.lock.yml`, `cross-repo-issue-analysis.lock.yml`
diff --git a/docs/features/session-persistence.md b/docs/features/session-persistence.md
index 302844b347..efb4403669 100644
--- a/docs/features/session-persistence.md
+++ b/docs/features/session-persistence.md
@@ -283,7 +283,17 @@ When resuming a session, you can optionally reconfigure many settings. This is u
With `model: "auto"`, the optional `capi.autoTier` setting selects an Auto routing preference: `efficiency`, `balance`, `intelligence`, or `fast`. In Python, use `capi={"auto_tier": "balance"}`. This setting applies to V2 Auto routing; V1 Auto requests are unchanged.
-`fast` is an integrator-only latency preset, not a first-party GitHub Copilot product preference. The SDK does not decide Fast eligibility, inspect client identity, choose it as a default, or fall back to another tier when a runtime does not support it—an older runtime returns its native error unchanged.
+When the runtime's default-off `DYNAMIC_AUTO_TIERS` feature is enabled, the session's `model.list` RPC also returns optional `auto` metadata from the provider's `/meta` endpoint. Use the enabled descriptors with `type: "auto"` to discover additional supported tier identifiers, their display names, descriptions, and ordering. The existing selection methods and JSON fields accept these identifiers. A discovery failure does not fall back to `/models`, and a successful response without `auto` metadata provides no selectable Auto tiers.
+
+Treat tier identifiers as extensible values, not a closed enumeration. Existing named values remain available; for additional identifiers, use strings in Node.js and Python, `AutoTier("premium-v2")` in Go, `new AutoTier("premium-v2")` in .NET, `AutoTier::Custom` in Rust, or `AutoTier.fromValue` in Java. The provider's `defaultTier` describes its default routing without creating a committed SDK preference.
+
+If a persisted tier is removed or disabled, resume preserves the explicit preference rather than replacing it with the provider's default. Select an available replacement before activating Auto routing.
+
+The CLI also preserves settings-derived preferences during startup, even when a tier is unavailable or dynamic discovery is disabled. You can then select a replacement in the model picker. Explicit SDK create and resume preferences are still validated before the operation succeeds, and Auto activation always validates availability against the resolved discovery mode.
+
+When a create or resume request includes `expAssignments`, the runtime installs those assignments before validating an explicit Auto preference. Validation, discovery, and activation use the same resolved feature decision.
+
+`fast` is an integrator-only latency preset, not a first-party GitHub Copilot product preference. Fast is not validated against the `/meta` tier catalog when configuring a session. The SDK does not decide Fast eligibility, inspect client identity, choose it as a default, or fall back to another tier when a runtime does not support it—an older runtime returns its native error unchanged.
The runtime persists the selected tier, so applications do not need to resend it on every resume:
@@ -309,6 +319,8 @@ if (result.status === "pending") {
The runtime does not apply the preference immediately. It records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider. A `pending` status therefore confirms that the request was accepted, not that it took effect. Only the most recent request survives: a new request replaces any earlier one that no turn has claimed yet.
+If an older request is still validating when a newer preference request or reset arrives, the older call fails with a superseded-request error instead of replacing the newer intent.
+
Watch for the outcome through these events:
* `session.model_change` when the preference commits.
diff --git a/docs/features/skills.md b/docs/features/skills.md
index 30c5112dbd..df8d319928 100644
--- a/docs/features/skills.md
+++ b/docs/features/skills.md
@@ -336,6 +336,333 @@ The frontmatter fields:
The markdown body contains the instructions that are injected into the session context when the skill is loaded.
+## Skill providers (experimental)
+
+> [!NOTE]
+> Skill providers are experimental. The API can change in future SDK releases.
+
+A skill provider serves skills from your application's own storage, such as a database in a multi-tenant service, instead of `SKILL.md` files on disk. Provider skills join the session's skill catalog alongside file-based skills. The model loads them on demand through the `skill` tool, and users can invoke them like any other skill.
+
+A provider implements two operations:
+
+* **List skills**: returns catalog metadata for every skill: a `name`, a `description`, and optionally `userInvocable`, `disableModelInvocation`, and `argumentHint`. The runtime calls it when it loads the session's skills.
+* **Read skill**: returns the markdown for one skill, or a not-found result if the skill no longer exists. The runtime calls it only when the skill is loaded.
+
+Pass the provider when you create or resume a session:
+
+
+Node.js / TypeScript
+
+```typescript
+import { approveAll, CopilotClient, type SkillProvider } from "@github/copilot-sdk";
+
+const releaseSkills = new Map([
+ [
+ "release-notes",
+ {
+ description: "Writes release notes in the team's format.",
+ markdown: "Group changes by feature area and link each pull request.",
+ },
+ ],
+]);
+
+const skillProvider: SkillProvider = {
+ listSkills: () =>
+ [...releaseSkills].map(([name, skill]) => ({ name, description: skill.description })),
+ readSkill: (name) => releaseSkills.get(name)?.markdown ?? null,
+};
+
+const client = new CopilotClient();
+const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ skillProvider,
+});
+```
+
+
+
+Python
+
+```python
+from copilot import CopilotClient, SkillProviderDescriptor
+from copilot.session import PermissionHandler
+
+RELEASE_SKILLS = {
+ "release-notes": (
+ "Writes release notes in the team's format.",
+ "Group changes by feature area and link each pull request.",
+ ),
+}
+
+
+class ReleaseSkills:
+ async def list_skills(self) -> list[SkillProviderDescriptor]:
+ return [
+ SkillProviderDescriptor(name=name, description=description)
+ for name, (description, _) in RELEASE_SKILLS.items()
+ ]
+
+ async def read_skill(self, name: str) -> str | None:
+ skill = RELEASE_SKILLS.get(name)
+ return skill[1] if skill else None
+
+
+async def main():
+ client = CopilotClient()
+ await client.start()
+ session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ skill_provider=ReleaseSkills(),
+ )
+```
+
+
+
+Go
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ copilot "github.com/github/copilot-sdk/go"
+ "github.com/github/copilot-sdk/go/rpc"
+)
+
+type releaseSkill struct {
+ description string
+ markdown string
+}
+
+type releaseSkills map[string]releaseSkill
+
+func (s releaseSkills) ListSkills(ctx context.Context) ([]rpc.SkillProviderDescriptor, error) {
+ descriptors := make([]rpc.SkillProviderDescriptor, 0, len(s))
+ for name, skill := range s {
+ descriptors = append(descriptors, rpc.SkillProviderDescriptor{Name: name, Description: skill.description})
+ }
+ return descriptors, nil
+}
+
+func (s releaseSkills) ReadSkill(ctx context.Context, name string) (string, error) {
+ skill, ok := s[name]
+ if !ok {
+ return "", fmt.Errorf("%w: %s", copilot.ErrSkillNotFound, name)
+ }
+ return skill.markdown, nil
+}
+
+func main() {
+ ctx := context.Background()
+ client := copilot.NewClient(nil)
+ session, err := client.CreateSession(ctx, &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ SkillProvider: releaseSkills{
+ "release-notes": {
+ description: "Writes release notes in the team's format.",
+ markdown: "Group changes by feature area and link each pull request.",
+ },
+ },
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ _ = session
+}
+```
+
+
+
+.NET
+
+```csharp
+#pragma warning disable GHCP001 // Skill providers are experimental.
+using GitHub.Copilot;
+using GitHub.Copilot.Rpc;
+
+public sealed class ReleaseSkills : ISkillProvider
+{
+ private readonly Dictionary _skills = new()
+ {
+ ["release-notes"] = (
+ "Writes release notes in the team's format.",
+ "Group changes by feature area and link each pull request."),
+ };
+
+ public Task> ListSkillsAsync(CancellationToken cancellationToken) =>
+ Task.FromResult>(
+ _skills.Select(skill => new SkillProviderDescriptor
+ {
+ Name = skill.Key,
+ Description = skill.Value.Description,
+ }).ToList());
+
+ public Task ReadSkillAsync(string name, CancellationToken cancellationToken) =>
+ Task.FromResult(_skills.TryGetValue(name, out var skill) ? skill.Markdown : null);
+}
+
+public static class SkillProviderExample
+{
+ public static async Task RunAsync(CopilotClient client)
+ {
+ await using var session = await client.CreateSessionAsync(new SessionConfig
+ {
+ OnPermissionRequest = PermissionHandler.ApproveAll,
+ SkillProvider = new ReleaseSkills(),
+ });
+ }
+}
+```
+
+The .NET skill provider types raise the `GHCP001` experimental diagnostic. Suppress it with `#pragma warning disable GHCP001` or a project-level `GHCP001`.
+
+
+
+Java
+
+```java
+import com.github.copilot.AllowCopilotExperimental;
+import com.github.copilot.CopilotClient;
+import com.github.copilot.CopilotSession;
+import com.github.copilot.SkillProvider;
+import com.github.copilot.SkillProviderDescriptor;
+import com.github.copilot.rpc.PermissionHandler;
+import com.github.copilot.rpc.SessionConfig;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+@AllowCopilotExperimental
+class ReleaseSkills implements SkillProvider {
+ private final Map markdown = Map.of(
+ "release-notes", "Group changes by feature area and link each pull request.");
+
+ @Override
+ public CompletableFuture> listSkills() {
+ return CompletableFuture.completedFuture(List.of(new SkillProviderDescriptor(
+ "release-notes", "Writes release notes in the team's format.", null, null, null)));
+ }
+
+ @Override
+ public CompletableFuture readSkill(String name) {
+ return CompletableFuture.completedFuture(markdown.get(name));
+ }
+}
+
+@AllowCopilotExperimental
+class SkillProviderExample {
+ static CopilotSession createSession(CopilotClient client) throws Exception {
+ return client.createSession(new SessionConfig()
+ .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
+ .setSkillProvider(new ReleaseSkills()))
+ .get();
+ }
+}
+```
+
+The Java skill provider API is `@CopilotExperimental`, so the consuming class or method must opt in with `@AllowCopilotExperimental` (or compile with `-Acopilot.experimental.allowed=true`). See [Using experimental APIs](../../java/README.md#using-experimental-apis).
+
+
+
+Rust
+
+
+```rust
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use github_copilot_sdk::{Error, SessionConfig, SkillProvider, SkillProviderDescriptor};
+
+struct ReleaseSkills;
+
+#[async_trait]
+impl SkillProvider for ReleaseSkills {
+ async fn list_skills(&self) -> Result, Error> {
+ Ok(vec![SkillProviderDescriptor {
+ name: "release-notes".into(),
+ description: "Writes release notes in the team's format.".into(),
+ ..Default::default()
+ }])
+ }
+
+ async fn read_skill(&self, name: &str) -> Result
+
+### Provider skill content
+
+Read skill returns the skill's `SKILL.md` text. YAML frontmatter is optional:
+
+* Without frontmatter, the whole text is the skill body, and all metadata comes from the listed skill.
+* With frontmatter, an omitted field inherits the listed value. A field that you include must match the listed value, or the skill fails to load.
+* `allowed-tools` is read only from frontmatter.
+* If the first line of the text is `---`, the runtime parses it as frontmatter, so don't start a body-only skill with a Markdown thematic break.
+
+Provider skills are text-only. They have no base directory, so they can't reference bundled scripts, templates, or other files. `skills.list` reports them with the source `sdk` and an empty `path`.
+
+### Serving existing SKILL.md files
+
+To serve `SKILL.md` files that you already have, such as files stored in a database, parse each file's YAML frontmatter once to build its catalog entry, then return the file unchanged from read skill:
+
+* Map `name` and `description` to the listed skill's `name` and `description`. Both are required in the catalog; the runtime doesn't fall back to a folder name or the skill body.
+* Map `user-invocable`, `disable-model-invocation`, and `argument-hint` to the matching optional fields when the frontmatter sets them.
+* Return the original text, frontmatter included, from read skill. Because the listed values came from the same frontmatter, they match, and the runtime still reads `allowed-tools` from it.
+
+### Trusting provider content
+
+Treat provider skills like skill directories: their content is trusted input. The model follows a skill's instructions, and the frontmatter controls how the skill is invoked. `allowed-tools` doesn't grant permissions in SDK sessions. The runtime reports it in the `allowedTools` field of the `skill.invoked` event, and your permission handler still decides every tool request.
+
+Don't serve text that end users or other tenants can edit unless you would let them author a skill file. If you build skills from user input, generate the frontmatter yourself instead of passing user-supplied frontmatter through.
+
+### Provider limits and errors
+
+The runtime validates the provider's catalog and content:
+
+* Names must match `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$` and be unique, ignoring case.
+* A catalog can contain at most 1,024 skills and 1 MiB of metadata. Each description and argument hint can be at most 1,024 characters.
+* Each skill's markdown can be at most 1 MiB of UTF-8.
+* Each call must complete within 30 seconds. When the runtime stops waiting for a call, because it timed out, the session ended, or a resume replaced the provider, it cancels the request and ignores any later result. Each SDK passes that cancellation to your provider in its usual way:
+
+ | SDK | Cancellation signal |
+ |---|---|
+ | Node.js | The `signal` (`AbortSignal`) in the options passed to `listSkills` and `readSkill` is aborted. |
+ | Python | The provider's `asyncio` task is cancelled with `asyncio.CancelledError`. Synchronous providers run to completion. |
+ | Go | The call's `context.Context` is cancelled. |
+ | .NET | The call's `CancellationToken` is cancelled. |
+ | Java | The returned `CompletableFuture` is cancelled with `cancel(true)`, which doesn't interrupt running work. |
+ | Rust | The provider future is dropped. |
+
+ These signals cover calls that the runtime cancels. When the connection closes or the client is force-stopped, the .NET, Java, and Rust SDKs also cancel calls that are still running. The Node.js, Python, and Go SDKs don't: a running call continues until it returns, and the SDK discards its result.
+
+If listing fails or returns an invalid catalog, the runtime reports the problem in the `errors` list returned by `skills.reload`. A failed reload keeps the provider skills from the last successful list. If reading a skill fails, the `skill` tool reports a generic load failure to the model. The SDK never forwards the text of errors your provider throws or returns, so that text can't leak into the conversation.
+
+### Provider lifecycle
+
+A provider is bound to one session:
+
+* A provider enables skills unless you set `enableSkills` to `false`, which keeps the provider bound but never called. In `mode: "empty"`, skills stay disabled until you set `enableSkills` to `true`.
+* The provider is never persisted. Pass it again when you resume a session. Resuming without a provider removes it from the session. Extensions that join the session don't change it.
+* A provider skill with the same name as a file-based skill replaces the file-based skill, and `skills.reload` reports a warning.
+* Sub-agents use their parent session's provider.
+* The runtime can call the provider concurrently, for example from the `skill` tool, user invocations, and custom agents that preload skills, so both operations must be safe for concurrent use.
+* Cloud sessions don't support skill providers. The SDK rejects the configuration before it creates the session.
+
## Configuration options
### SessionConfig skill fields
@@ -344,12 +671,18 @@ The markdown body contains the instructions that are injected into the session c
|----------|-------|------|-------------|
| Node.js | `skillDirectories` | `string[]` | Directories to load skills from |
| Node.js | `disabledSkills` | `string[]` | Skills to disable |
+| Node.js | `skillProvider` | `SkillProvider` | Experimental provider for SDK-supplied skills |
| Python | `skill_directories` | `list[str]` | Directories to load skills from |
| Python | `disabled_skills` | `list[str]` | Skills to disable |
+| Python | `skill_provider` | `SkillProvider` | Experimental provider for SDK-supplied skills |
| Go | `SkillDirectories` | `[]string` | Directories to load skills from |
| Go | `DisabledSkills` | `[]string` | Skills to disable |
+| Go | `SkillProvider` | `SkillProvider` | Experimental provider for SDK-supplied skills |
| .NET | `SkillDirectories` | `List` | Directories to load skills from |
| .NET | `DisabledSkills` | `List` | Skills to disable |
+| .NET | `SkillProvider` | `ISkillProvider` | Experimental provider for SDK-supplied skills |
+| Java | `setSkillProvider` | `SkillProvider` | Experimental provider for SDK-supplied skills |
+| Rust | `with_skill_provider` | `Arc` | Experimental provider for SDK-supplied skills |
### Built-in skills and `mode: "empty"`
diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md
index 514a8b2b0e..dbb6111897 100644
--- a/docs/features/streaming-events.md
+++ b/docs/features/streaming-events.md
@@ -856,7 +856,7 @@ A skill was activated for the current conversation.
| `name` | `string` | ✅ | Skill name |
| `path` | `string` | ✅ | File path to the SKILL.md definition |
| `content` | `string` | ✅ | Full skill content injected into the conversation |
-| `allowedTools` | `string[]` | | Tools auto-approved while this skill is active |
+| `allowedTools` | `string[]` | | Tools listed in the skill's `allowed-tools` frontmatter. The SDK doesn't approve them automatically; your permission handler still decides. The Copilot CLI auto-approves them while the skill is active. |
| `pluginName` | `string` | | Plugin the skill originated from |
| `pluginVersion` | `string` | | Plugin version |
diff --git a/dotnet/README.md b/dotnet/README.md
index 6bde71b71c..1269c6c057 100644
--- a/dotnet/README.md
+++ b/dotnet/README.md
@@ -207,6 +207,7 @@ Create a new conversation session.
- `WorkingDirectory` - Working directory for the session. When not set, the runtime uses its own process working directory.
- `EnableSessionStore` - Enables the cross-session store for search and retrieval across sessions. When unset in `CopilotClientMode.CopilotCli`, the runtime default applies (enabled). In `CopilotClientMode.Empty`, defaults to disabled.
- `GitHubTokenProvider` - Acquires session-scoped GitHub tokens on demand. Return `GitHubTokenProviderResult.FromToken` with a positive `ExpiresIn` value (production GitHub tokens typically use `8 * 60 * 60` seconds), or `GitHubTokenProviderResult.Cancel()`. Cannot be combined with `GitHubToken`.
+- `SkillProvider` - Experimental session-scoped skill provider. See [Skill providers (experimental)](#skill-providers-experimental).
- `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. `PermissionHandler.ApproveAll` approves requests when managed settings are disabled and throws when `EnableManagedSettings` is true. Custom handlers can inspect `ManagedApprovalRequired` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section.
- `OnUserInputRequest` - Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section.
- `AskUserVariant` - Selects the model-facing `ask_user` tool shape. Defaults to `AskUserVariant.Legacy`; use `AskUserVariant.Elicitation` with `OnElicitationRequest`.
@@ -225,6 +226,7 @@ Resume an existing session. Returns the session with `WorkspacePath` populated i
- `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section.
- `GitHubTokenProvider` - Replaces the session-scoped token provider when resuming. Cannot be combined with `GitHubToken`.
+- `SkillProvider` - Re-supplies the session-scoped skill provider when resuming.
- `AskUserVariant` - Re-supplies the model-facing `ask_user` tool shape on cold resume.
- `AllowTranscriptRecovery` - Repairs a damaged transcript when true. The default
is true in all modes; set false to reject recovery. `session.TranscriptRecovery` contains
@@ -254,6 +256,62 @@ await using var session = await client.CreateSessionAsync(new SessionConfig
Initial acquisition runs during session creation or resume. Cancellation, provider errors, and invalid token responses reject that operation instead of falling back to ambient authentication. Idle sessions refresh only before their next credential-consuming operation; there is no background refresh timer.
+##### Skill providers (experimental)
+
+Set `SessionConfig.SkillProvider` or `ResumeSessionConfig.SkillProvider` to
+serve session-scoped skills from your application. The provider is not persisted:
+pass it again on every resume, because resuming without one unbinds it. Skill
+providers are only supported for local sessions; `CreateSessionAsync` throws if
+`Cloud` and `SkillProvider` are both set. The skill provider types are
+experimental and raise the `GHCP001` diagnostic; suppress it with
+`#pragma warning disable GHCP001` or `GHCP001`.
+
+```csharp
+#pragma warning disable GHCP001 // Skill providers are experimental.
+
+public sealed class MySkillProvider : ISkillProvider
+{
+ public Task> ListSkillsAsync(CancellationToken cancellationToken) =>
+ Task.FromResult>(
+ [
+ new()
+ {
+ Name = "project-facts",
+ Description = "Important facts about this host application",
+ ArgumentHint = ""
+ }
+ ]);
+
+ public Task ReadSkillAsync(string name, CancellationToken cancellationToken) =>
+ Task.FromResult(name == "project-facts"
+ ? """
+ # Project facts
+
+ Use the application's project index before answering.
+ """
+ : null);
+}
+
+await using var client = new CopilotClient(new CopilotClientOptions
+{
+ Mode = CopilotClientMode.Empty
+});
+
+await using var session = await client.CreateSessionAsync(new SessionConfig
+{
+ AvailableTools = [], // Empty mode requires an explicit tool allow-list.
+ SkillProvider = new MySkillProvider(),
+ EnableSkills = true // Required in empty mode; otherwise skills default off.
+});
+```
+
+The runtime may call `ListSkillsAsync` and `ReadSkillAsync` concurrently. Honor
+the supplied cancellation token; it is canceled when the runtime abandons the
+request (for example, on timeout, session disposal, or a resume that replaces the
+provider) or the connection closes. Return `null` from `ReadSkillAsync` when a
+skill name is not found. Optional `SkillProviderDescriptor` properties are
+omitted from JSON when unset.
+
##### `PingAsync(string? message = null): Task`
Ping the server to check connectivity.
diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs
index 999440992f..ab4399ef5c 100644
--- a/dotnet/src/Client.cs
+++ b/dotnet/src/Client.cs
@@ -892,6 +892,7 @@ private CopilotSession InitializeSession(
session.RegisterElicitationHandler(config.OnElicitationRequest);
session.RegisterExitPlanModeHandler(config.OnExitPlanModeRequest);
session.RegisterAutoModeSwitchHandler(config.OnAutoModeSwitchRequest);
+ session.RegisterSkillProvider(config.SkillProvider);
if (config.OnUserInputRequest != null)
{
session.RegisterUserInputHandler(config.OnUserInputRequest);
@@ -1190,6 +1191,10 @@ public async Task CreateSessionAsync(SessionConfig config, Cance
{
ArgumentNullException.ThrowIfNull(config);
ValidateGitHubTokenConfig(config);
+ if (config.Cloud is not null && config.SkillProvider is not null)
+ {
+ throw new ArgumentException("Skill providers are not supported for cloud sessions.");
+ }
var connection = await EnsureConnectedAsync(cancellationToken);
var totalTimestamp = Stopwatch.GetTimestamp();
@@ -1327,7 +1332,8 @@ public async Task CreateSessionAsync(SessionConfig config, Cance
GitHubMcpToolConfig: config.GitHubMcpToolConfig,
ManagedSettings: config.ManagedSettings,
EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null,
- AdditionalDirectories: config.AdditionalDirectories);
+ AdditionalDirectories: config.AdditionalDirectories,
+ HasSkillProvider: config.SkillProvider is not null ? true : null);
var rpcTimestamp = Stopwatch.GetTimestamp();
@@ -1593,7 +1599,8 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes
ManagedSettings: config.ManagedSettings,
EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null,
AdditionalDirectories: config.AdditionalDirectories,
- AllowTranscriptRecovery: config.AllowTranscriptRecovery);
+ AllowTranscriptRecovery: config.AllowTranscriptRecovery,
+ HasSkillProvider: config.SkillProvider is not null ? true : null);
var rpcTimestamp = Stopwatch.GetTimestamp();
var response = await InvokeRpcAsync(
@@ -1807,6 +1814,7 @@ public async Task DeleteSessionAsync(string sessionId, CancellationToken cancell
if (_sessions.TryRemove(sessionId, out var session))
{
+ session.ClearSkillProvider();
session.ReleaseGitHubTokenProviderRegistration();
}
}
@@ -2753,6 +2761,8 @@ private async Task ConnectToServerAsync(Process? cliProcess, string?
rpc.SetLocalRpcMethod("autoModeSwitch.request", handler.OnAutoModeSwitchRequest);
rpc.SetLocalRpcMethod("hooks.invoke", handler.OnHooksInvoke);
rpc.SetLocalRpcMethod("systemMessage.transform", handler.OnSystemMessageTransform);
+ rpc.SetLocalRpcMethod("skillProvider.list", handler.OnSkillProviderList, singleObjectParam: true);
+ rpc.SetLocalRpcMethod("skillProvider.read", handler.OnSkillProviderRead, singleObjectParam: true);
ClientSessionApiRegistration.RegisterClientSessionApiHandlers(rpc, sessionId =>
{
var session = GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
@@ -2864,6 +2874,7 @@ private void CancelPendingExternalTools()
}
foreach (var session in _sessions.Values)
{
+ session.ClearSkillProvider();
session.CancelPendingExternalTools();
}
}
@@ -3039,6 +3050,32 @@ public async ValueTask OnSystemMessageTransfo
return await session.HandleSystemMessageTransformAsync(sections);
}
+ public async ValueTask OnSkillProviderList(SkillProviderListRequest request, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+ if (string.IsNullOrEmpty(request.SessionId))
+ {
+ throw new ArgumentException("Skill provider list request is missing a session id.");
+ }
+
+ var session = client.GetSession(request.SessionId)
+ ?? throw new InvalidOperationException($"No skill provider for session: {request.SessionId}");
+ return await session.HandleSkillProviderListAsync(cancellationToken);
+ }
+
+ public async ValueTask OnSkillProviderRead(SkillProviderReadRequest request, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+ if (string.IsNullOrEmpty(request.SessionId) || string.IsNullOrEmpty(request.Name))
+ {
+ throw new ArgumentException("Skill provider read request is missing a session id or skill name.");
+ }
+
+ var session = client.GetSession(request.SessionId)
+ ?? throw new InvalidOperationException($"No skill provider for session: {request.SessionId}");
+ return await session.HandleSkillProviderReadAsync(request.Name, cancellationToken);
+ }
+
}
private class Connection(
@@ -3197,7 +3234,8 @@ internal record CreateSessionRequest(
[property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null,
bool? EnableGitHubTelemetryForwarding = null,
[property: JsonPropertyName("githubMcpToolConfig")] GitHubMcpToolConfig? GitHubMcpToolConfig = null,
- IList? AdditionalDirectories = null);
+ IList? AdditionalDirectories = null,
+ bool? HasSkillProvider = null);
#pragma warning restore GHCP001
internal record ToolDefinition(
@@ -3319,7 +3357,8 @@ internal record ResumeSessionRequest(
bool? EnableGitHubTelemetryForwarding = null,
[property: JsonPropertyName("githubMcpToolConfig")] GitHubMcpToolConfig? GitHubMcpToolConfig = null,
IList? AdditionalDirectories = null,
- bool? AllowTranscriptRecovery = null);
+ bool? AllowTranscriptRecovery = null,
+ bool? HasSkillProvider = null);
#pragma warning restore GHCP001
internal record ResumeSessionResponse(
@@ -3407,6 +3446,21 @@ internal record UserInputRequestResponse(
string Answer,
bool WasFreeform);
+ internal record SkillProviderListRequest(
+ string SessionId);
+
+#pragma warning disable GHCP001
+ internal record SkillProviderListResult(
+ IReadOnlyList Skills);
+#pragma warning restore GHCP001
+
+ internal record SkillProviderReadRequest(
+ string SessionId,
+ string Name);
+
+ internal record SkillProviderReadResult(
+ [property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] string? Markdown);
+
internal record AutoModeSwitchRequestResponse(
AutoModeSwitchResponse Response);
@@ -3435,6 +3489,12 @@ internal record HooksInvokeResponse(
[JsonSerializable(typeof(ExitPlanModeResult))]
[JsonSerializable(typeof(GetLastSessionIdResponse))]
[JsonSerializable(typeof(HooksInvokeResponse))]
+ [JsonSerializable(typeof(SkillProviderListRequest))]
+#pragma warning disable GHCP001
+ [JsonSerializable(typeof(SkillProviderListResult))]
+#pragma warning restore GHCP001
+ [JsonSerializable(typeof(SkillProviderReadRequest))]
+ [JsonSerializable(typeof(SkillProviderReadResult))]
[JsonSerializable(typeof(ListSessionsRequest))]
[JsonSerializable(typeof(ListSessionsResponse))]
[JsonSerializable(typeof(GetSessionMetadataRequest))]
diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs
index 080abf0116..1fb01ec033 100644
--- a/dotnet/src/Generated/Rpc.cs
+++ b/dotnet/src/Generated/Rpc.cs
@@ -879,6 +879,10 @@ public sealed class Model
[JsonPropertyName("supportedReasoningEfforts")]
public IList? SupportedReasoningEfforts { get; set; }
+ /// Model vendor as the Copilot API reports it, for example "Anthropic" or "Azure OpenAI". Open vocabulary, passed through unchanged. It can name the vendor that serves the model instead of the one that built it, or a label that is not a vendor, such as "Experimental". Absent when the Copilot API reports no vendor.
+ [JsonPropertyName("vendor")]
+ public string? Vendor { get; set; }
+
/// Warnings the service published for this model, such as a deprecated client version. Present only when the service published at least one warning. The model remains usable; hosts should surface these as advisory rather than blocking.
[JsonPropertyName("warningMessages")]
public IList? WarningMessages { get; set; }
@@ -932,7 +936,7 @@ public sealed class BuiltInModelCatalog
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
public sealed class SandboxHostCapability
{
- /// The policy feature, as an extensible string: ignore names you do not recognize. Known values: `network` (sandboxed commands can reach the network; on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns), `network_filtering` (host rules and the sandbox proxy; on Linux this needs the same tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback support, and a policy that uses it must also set `network.allowLocalNetwork`), `denied_paths` (native enforcement of `filesystem.deniedPaths`), `shell` (shell commands inside the sandbox), and `filesystem_enumeration` (enumerate-only filesystem grants; on Windows this needs Process Security Environment 1.1 filesystem enumeration support, and without it sandboxed PowerShell still runs but cannot resolve its current location; other platforms always report it).
+ /// The policy feature, as an extensible string: ignore names you do not recognize. Known values: `network` (sandboxed commands can reach the network; on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns), `network_filtering` (host rules and the sandbox proxy; on Linux this needs the same tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback support or MXC's PSEC 1.0-only proxy-loopback compatibility capability, and a policy that uses it must also set `network.allowLocalNetwork`; compatibility applies only to an explicit identity-less runtime proxy, not general host-loopback access, and other policy restrictions still apply), `denied_paths` (native enforcement of `filesystem.deniedPaths`), `shell` (shell commands inside the sandbox), and `filesystem_enumeration` (enumerate-only filesystem grants; on Windows this needs Process Security Environment 1.1 filesystem enumeration support, and without it sandboxed PowerShell still runs but cannot resolve its current location; other platforms always report it).
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
@@ -1309,6 +1313,7 @@ internal sealed class AccountGetQuotaRequest
[JsonDerivedType(typeof(AuthInfoTokenProvider), "token-provider")]
[JsonDerivedType(typeof(AuthInfoCopilotApiToken), "copilot-api-token")]
[JsonDerivedType(typeof(AuthInfoUser), "user")]
+[JsonDerivedType(typeof(AuthInfoAccount), "account")]
[JsonDerivedType(typeof(AuthInfoGhCli), "gh-cli")]
[JsonDerivedType(typeof(AuthInfoApiKey), "api-key")]
public partial class AuthInfo
@@ -1805,6 +1810,24 @@ public partial class AuthInfoUser : AuthInfo
public required string Login { get; set; }
}
+/// An interactive account whose model provider owns its credentials. It carries no GitHub credential.
+/// The account variant of .
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public partial class AuthInfoAccount : AuthInfo
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "account";
+
+ /// Host coordinate owned by the account's model provider.
+ [JsonPropertyName("host")]
+ public required string Host { get; set; }
+
+ /// Login identifying the provider-owned account.
+ [JsonPropertyName("login")]
+ public required string Login { get; set; }
+}
+
/// Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value.
/// The gh-cli variant of .
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
@@ -6754,6 +6777,128 @@ internal sealed class AgentsGetDiscoveryPathsRequest
public IList? ProjectPaths { get; set; }
}
+/// The agents this runtime ships, named so a consumer can tell them apart from authored ones.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class AgentsGetBuiltinsResult
+{
+ /// The subset of `names` a user is allowed to turn off. A shipped agent outside this list is always active and a client should not offer a toggle for it.
+ [JsonPropertyName("disableableNames")]
+ public IList DisableableNames { get => field ??= []; set; }
+
+ /// Every agent name this runtime ships.
+ [JsonPropertyName("names")]
+ public IList Names { get => field ??= []; set; }
+
+ /// The subset of `names` defined by a shipped YAML definition. The remainder are special-cased in code and have no definition to load.
+ [JsonPropertyName("yamlBasedNames")]
+ public IList YamlBasedNames { get => field ??= []; set; }
+}
+
+/// A shipped agent, named and described.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class BuiltinAgentSummary
+{
+ /// One-line description of what the agent does.
+ [JsonPropertyName("description")]
+ public string Description { get; set; } = string.Empty;
+
+ /// The agent name, as it appears in `getBuiltins`.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+}
+
+/// The shipped agents available under the requested flags.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class AgentsGetAvailableBuiltinsResult
+{
+ /// Available shipped agents, in the runtime's own order.
+ [JsonPropertyName("agents")]
+ public IList Agents { get => field ??= []; set; }
+}
+
+/// The feature flags to evaluate shipped agents against.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class AgentsGetAvailableBuiltinsRequest
+{
+ /// The surface asking, which gates agents that only apply to one client. Omit or pass null to apply no client filter.
+ [JsonPropertyName("context")]
+ public string? Context { get; set; }
+
+ /// Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or pass null for no flags.
+ [JsonPropertyName("featureFlags")]
+ public IDictionary? FeatureFlags { get; set; }
+
+ /// Flag overrides keyed by name. A null entry uses the corresponding base flag; false explicitly disables it. Omit or pass null for no overrides.
+ [JsonPropertyName("overrides")]
+ public IDictionary? Overrides { get; set; }
+}
+
+/// One shipped agent's definition.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class AgentsGetBuiltinDefinitionResult
+{
+ /// The agent's definition, serialized as JSON. It carries the authored keys plus the runtime's projected `__nativeCustomAgent` view of the same agent. It is a string rather than an object because the runtime parses it with the agent schema's tolerant shape, which accepts keys this contract does not name.
+ [JsonPropertyName("definitionJson")]
+ public string DefinitionJson { get; set; } = string.Empty;
+}
+
+/// The shipped agent whose definition to load.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class AgentsGetBuiltinDefinitionRequest
+{
+ /// The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside that list is special-cased in code and has no definition, and is reported as an error rather than as an empty definition.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+}
+
+/// One shipped agent, projected for a listing.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class AgentsGetBuiltinListingDefinitionResult
+{
+ /// The agent projected as a custom agent, serialized as JSON. It is a string rather than an object for the same reason as `getBuiltinDefinition`: the runtime parses the underlying definition with the agent schema's tolerant shape, which accepts keys this contract does not name.
+ [JsonPropertyName("definitionJson")]
+ public string DefinitionJson { get; set; } = string.Empty;
+}
+
+/// The shipped agent whose listing entry to load.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class AgentsGetBuiltinListingDefinitionRequest
+{
+ /// The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here too, from its in-code definition.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+}
+
+/// The model to switch to, and the warning to show when the agent's preference could not be met.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class AgentsCustomAgentInitialModelDecisionResult
+{
+ /// The reasoning effort attached to the selected model preference. Absent when that preference does not specify an effort.
+ [JsonPropertyName("reasoningEffort")]
+ public string? ReasoningEffort { get; set; }
+
+ /// The first available model that matches the agent's preferences. Absent when none of the requested models is available.
+ [JsonPropertyName("targetModel")]
+ public string? TargetModel { get; set; }
+
+ /// What to tell the user about an unmet preference. Absent when the preference was met. A warning with no `targetModel` means the agent's models are all unavailable.
+ [JsonPropertyName("warning")]
+ public string? Warning { get; set; }
+}
+
+/// The models a custom agent asks for, and the models actually available.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class AgentsCustomAgentInitialModelDecisionParams
+{
+ /// The agent's declared `model:` entry, serialized. A single name or an ordered list of acceptable names.
+ [JsonPropertyName("agentModelsJson")]
+ public string AgentModelsJson { get; set; } = string.Empty;
+
+ /// The models available to this session, serialized in the shape the model list carries.
+ [JsonPropertyName("availableModelsJson")]
+ public string AvailableModelsJson { get; set; } = string.Empty;
+}
+
/// Loaded instruction source for a session, including path, content, category, location, applicability, and optional description.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
public sealed class InstructionSource
@@ -6868,6 +7013,179 @@ internal sealed class InstructionsGetDiscoveryPathsRequest
public IList? ProjectPaths { get; set; }
}
+/// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class InstalledPlugin
+{
+ /// Path where the plugin is cached locally.
+ [JsonPropertyName("cache_path")]
+ public string? CachePath { get; set; }
+
+ /// Whether the plugin is currently enabled.
+ [JsonPropertyName("enabled")]
+ public bool Enabled { get; set; }
+
+ /// Installation timestamp.
+ [JsonPropertyName("installed_at")]
+ public string InstalledAt { get; set; } = string.Empty;
+
+ /// Absolute path of the marketplace directory a live plugin was resolved from. Present only on live, never-persisted records — those synthesized at session start for a directory/local marketplace, whose cache_path points at the real plugin directory on disk rather than a copy under the installed-plugins cache. Its presence is what marks a record as live, and no record carrying it is ever written to the persisted installedPlugins key.
+ [JsonPropertyName("installed_from")]
+ public string? InstalledFrom { get; set; }
+
+ /// Marketplace the plugin came from (empty string for direct repo installs).
+ [JsonPropertyName("marketplace")]
+ public string Marketplace { get; set; } = string.Empty;
+
+ /// Plugin name.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ /// Source for direct repo installs (when marketplace is empty).
+ [JsonPropertyName("source")]
+ public JsonElement? Source { get; set; }
+
+ /// Per-plugin source fingerprint (a SHA-256 hash of the plugin's catalog source spec plus its resolved source subtree — NOT a Git commit SHA) captured at marketplace install/update time. Auto-update compares it against the freshly recomputed fingerprint to detect a content change that does not bump the version. Absent for pre-existing installs and for direct (non-marketplace) installs.
+ [JsonPropertyName("source_sha")]
+ public string? SourceSha { get; set; }
+
+ /// Version installed (if available).
+ [JsonPropertyName("version")]
+ public string? Version { get; set; }
+}
+
+/// An account the host has signed in to, identified by the server it lives on and the login it uses there. The same person can appear more than once when they use both github.com and an Enterprise server.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class LoggedInUser
+{
+ /// Source account this account was derived from, when one was recorded.
+ [JsonPropertyName("derivedFrom")]
+ public string? DerivedFrom { get; set; }
+
+ /// Host the account belongs to, such as `github.com` or an Enterprise server.
+ [JsonPropertyName("host")]
+ public string Host { get; set; } = string.Empty;
+
+ /// Account kind, when the host recorded one. Consumers must tolerate new strings.
+ [JsonPropertyName("kind")]
+ public string? Kind { get; set; }
+
+ /// Account login on that host.
+ [JsonPropertyName("login")]
+ public string Login { get; set; } = string.Empty;
+}
+
+/// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class GlobalStateLoadResult
+{
+ /// Whether the user has answered the prompt suggesting they install the desktop app.
+ [JsonPropertyName("appInstallNudgeResponded")]
+ public bool? AppInstallNudgeResponded { get; set; }
+
+ /// Whether the app tip has been shown.
+ [JsonPropertyName("appTipShown")]
+ public bool? AppTipShown { get; set; }
+
+ /// Terminals the user has already been asked to set up, so the host does not ask twice.
+ [JsonPropertyName("askedSetupTerminals")]
+ public IList? AskedSetupTerminals { get; set; }
+
+ /// When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the once-per-day cap for non-staff users across restarts.
+ [JsonPropertyName("autoFeedbackLastPromptedAt")]
+ public string? AutoFeedbackLastPromptedAt { get; set; }
+
+ /// When the host first ran on this machine.
+ [JsonPropertyName("firstLaunchAt")]
+ public string? FirstLaunchAt { get; set; }
+
+ /// Plugins installed on this machine.
+ [JsonPropertyName("installedPlugins")]
+ public IList? InstalledPlugins { get; set; }
+
+ /// Account used for the most recent sign-in.
+ [JsonPropertyName("lastLoggedInUser")]
+ public LoggedInUser? LastLoggedInUser { get; set; }
+
+ /// Every account the host has signed in to on this machine.
+ [JsonPropertyName("loggedInUsers")]
+ public IList? LoggedInUsers { get; set; }
+
+ /// Whether the one-off cleanup of stored reasoning summaries has run.
+ [JsonPropertyName("reasoningSummariesCleanupDone")]
+ public bool? ReasoningSummariesCleanupDone { get; set; }
+
+ /// Models the user selected recently, most recent first.
+ [JsonPropertyName("recentModelIds")]
+ public IList? RecentModelIds { get; set; }
+
+ /// Whether the user declined to trust the sandbox credential proxy CA.
+ [JsonPropertyName("sandboxCredentialProxyCaDeclined")]
+ public bool? SandboxCredentialProxyCaDeclined { get; set; }
+
+ /// Whether the sandbox onboarding has been shown.
+ [JsonPropertyName("sandboxOnboardingShown")]
+ public bool? SandboxOnboardingShown { get; set; }
+
+ /// Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only behavior.
+ [JsonPropertyName("staff")]
+ public bool? Staff { get; set; }
+
+ /// Whether the user was recognized as GitHub staff.
+ [JsonPropertyName("staffGithub")]
+ public bool? StaffGitHub { get; set; }
+
+ /// When the staff-only log level migration last ran.
+ [JsonPropertyName("staffLogLevelMigrationAt")]
+ public string? StaffLogLevelMigrationAt { get; set; }
+
+ /// Whether the user was recognized as Microsoft staff.
+ [JsonPropertyName("staffMicrosoft")]
+ public bool? StaffMicrosoft { get; set; }
+
+ /// When the staff-only model reset last ran.
+ [JsonPropertyName("staffModelResetAt")]
+ public string? StaffModelResetAt { get; set; }
+
+ /// When the staff-only update channel migration last ran.
+ [JsonPropertyName("staffUpdateChannelMigrationAt")]
+ public string? StaffUpdateChannelMigrationAt { get; set; }
+
+ /// Folders where the user declined the init prompt, so it stays hidden there.
+ [JsonPropertyName("suppressInitFolders")]
+ public IList? SuppressInitFolders { get; set; }
+
+ /// Folders the user has marked as trusted.
+ [JsonPropertyName("trustedFolders")]
+ public IList? TrustedFolders { get; set; }
+}
+
+/// Selects the configuration directory whose machine-wide state to read.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class GlobalStateLoadForConfigDirRequest
+{
+ /// Copilot configuration directory to read the state document from, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to read the directory the server resolved for itself.
+ [JsonPropertyName("configDir")]
+ public string? ConfigDir { get; set; }
+}
+
+/// A single top-level key to record in the host's machine-wide state. The write replaces only that key and leaves the rest of the document untouched, so two writers recording different one-off flags do not overwrite each other. The stored credential keys cannot be written through this method.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class GlobalStateWriteKeyRequest
+{
+ /// Copilot configuration directory to write the state document in, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to write the directory the server resolved for itself. Mirrors `globalState.loadForConfigDir`, so a caller can read and write the same directory.
+ [JsonPropertyName("configDir")]
+ public string? ConfigDir { get; set; }
+
+ /// Top-level key to write, named as it appears in the result of `globalState.load`. It must be one of the writable keys that `globalState.writeKey` lists.
+ [JsonPropertyName("key")]
+ public string Key { get; set; } = string.Empty;
+
+ /// Value to store for the key. Omit it, or pass null, to remove the key instead.
+ [JsonPropertyName("value")]
+ public JsonElement? Value { get; set; }
+}
+
/// A literal choice the command input accepts, with a human-facing description.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
public sealed class SlashCommandInputChoice
@@ -6969,7 +7287,7 @@ public sealed class UserSettingMetadata
public JsonElement Value { get; set; }
}
-/// Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides.
+/// Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
public sealed class UserSettingsGetResult
{
@@ -6978,15 +7296,6 @@ public sealed class UserSettingsGetResult
public IDictionary Settings { get => field ??= new Dictionary(); set; }
}
-/// Outcome of writing user settings.
-[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
-public sealed class UserSettingsSetResult
-{
- /// Top-level keys whose write landed in settings.json but is shadowed by a value still present in the legacy config.json (config.json wins on read). The write does not take effect until the legacy value is removed.
- [JsonPropertyName("shadowedKeys")]
- public IList ShadowedKeys { get => field ??= []; set; }
-}
-
/// Partial user settings to write to settings.json. Each top-level key is written individually, replacing the existing value; a key whose value is null is removed.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
internal sealed class UserSettingsSetRequest
@@ -6996,6 +7305,217 @@ internal sealed class UserSettingsSetRequest
public JsonElement Settings { get; set; }
}
+/// Owner, name, and host of a GitHub repository, as resolved from a git remote URL.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class GitHubRepositoryIdentity
+{
+ /// Host the remote points at, for example `github.com` or a GitHub Enterprise hostname.
+ [JsonPropertyName("host")]
+ public string Host { get; set; } = string.Empty;
+
+ /// Repository name, without the owner prefix or the `.git` suffix.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ /// Repository owner login (user or organization).
+ [JsonPropertyName("owner")]
+ public string Owner { get; set; } = string.Empty;
+}
+
+/// The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class GitHubRepositoryAtPathResult
+{
+ /// Resolved repository identity, or null when the selected remote resolves to no GitHub host.
+ [JsonPropertyName("repository")]
+ public GitHubRepositoryIdentity? Repository { get; set; }
+}
+
+/// Working-tree path whose owning GitHub repository should be resolved.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class GitHubRepositoryAtPathRequest
+{
+ /// Absolute path to a directory inside the git working tree to resolve.
+ [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("path")]
+ public string Path { get; set; } = string.Empty;
+}
+
+/// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class GitHubOwnersRequestIdResult
+{
+ /// Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`.
+ [JsonPropertyName("requestId")]
+ public long RequestId { get; set; }
+}
+
+/// A GitHub login the authenticated user may act as: their own account, or an organization they belong to.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class GitHubOwnerOption
+{
+ /// The owner's GitHub login.
+ [JsonPropertyName("login")]
+ public string Login { get; set; } = string.Empty;
+
+ /// Which kind of owner this is. The authenticated user's own account is always reported as `user`.
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = string.Empty;
+}
+
+/// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class GitHubOwnersListResult
+{
+ /// Why no owners could be listed, phrased for a user. Present when the listing failed in a way the caller should render rather than raise.
+ [JsonPropertyName("message")]
+ public string? Message { get; set; }
+
+ /// The owners, on success: the authenticated user first, then the organizations they belong to.
+ [JsonPropertyName("owners")]
+ public IList? Owners { get; set; }
+
+ /// A malformed request or an unreadable credential, which the caller raises instead of rendering. Kept a field rather than a dispatch error so it stays distinct from `message`, which the caller renders.
+ [JsonPropertyName("throwError")]
+ public string? ThrowError { get; set; }
+
+ /// A line the caller should log. Present only alongside `message`, and only for failures worth recording.
+ [JsonPropertyName("warning")]
+ public string? Warning { get; set; }
+}
+
+/// Credential to list owners under, and the request id that makes the listing cancellable.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class GitHubOwnersListRequest
+{
+ /// The credential the listing runs under, carried opaquely because its shape is the host's own and the runtime only resolves a token and a GitHub host from it. No credential travels: this selects one the runtime already holds.
+ [JsonPropertyName("authInfo")]
+ public JsonElement AuthInfo { get; set; }
+
+ /// Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled before use, released after being abandoned, or already used is refused rather than silently running uncancellable.
+ [JsonPropertyName("requestId")]
+ public long RequestId { get; set; }
+}
+
+/// Whether the id named a running owner listing.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class GitHubOwnersCancelResult
+{
+ /// True when a listing with the id was running and the cancel stopped it. False when the id was never registered, was registered but unused, was released after being abandoned, or its listing had ended. An unused id is released and cannot start a later listing.
+ [JsonPropertyName("canceled")]
+ public bool Canceled { get; set; }
+}
+
+/// The owner listing to abandon.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class GitHubOwnersCancelRequest
+{
+ /// Request id the listing was started with.
+ [JsonPropertyName("requestId")]
+ public long RequestId { get; set; }
+}
+
+/// The remote the checked-out branch tracks.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class GitCurrentBranchRemoteResult
+{
+ /// Name of the tracked remote. Reports `origin` whenever the working tree has no tracking configuration to read, including on a detached HEAD, so this is never null and never empty.
+ [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("remote")]
+ public string Remote { get; set; } = string.Empty;
+}
+
+/// Working-tree path a git query applies to.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class GitCwdRequest
+{
+ /// Absolute path to a directory inside the git working tree to query.
+ [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("cwd")]
+ public string Cwd { get; set; } = string.Empty;
+}
+
+/// Updated working directory and git context. Emitted as the new payload of `session.context_changed`.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class SessionWorkingDirectoryContext
+{
+ /// Merge-base commit SHA (fork point from the remote default branch).
+ [JsonPropertyName("baseCommit")]
+ public string? BaseCommit { get; set; }
+
+ /// Current git branch name.
+ [JsonPropertyName("branch")]
+ public string? Branch { get; set; }
+
+ /// Current working directory path.
+ [JsonPropertyName("cwd")]
+ public string Cwd { get; set; } = string.Empty;
+
+ /// Root directory of the git repository, resolved via git rev-parse.
+ [JsonPropertyName("gitRoot")]
+ public string? GitRoot { get; set; }
+
+ /// Head commit of the current git branch.
+ [JsonPropertyName("headCommit")]
+ public string? HeadCommit { get; set; }
+
+ /// Hosting platform type of the repository.
+ [JsonPropertyName("hostType")]
+ public SessionWorkingDirectoryContextHostType? HostType { get; set; }
+
+ /// Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps).
+ [JsonPropertyName("repository")]
+ public string? Repository { get; set; }
+
+ /// Raw host string from the git remote URL (e.g. "github.com", "dev.azure.com").
+ [JsonPropertyName("repositoryHost")]
+ public string? RepositoryHost { get; set; }
+}
+
+/// A GitHub repository one of a working tree's remotes points at.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class GitRemoteRepository
+{
+ /// GitHub host serving the repository, which is not `github.com` for a GitHub Enterprise remote.
+ [JsonPropertyName("host")]
+ public string Host { get; set; } = string.Empty;
+
+ /// Repository name, without the owner.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ /// Account or organization owning the repository.
+ [JsonPropertyName("owner")]
+ public string Owner { get; set; } = string.Empty;
+
+ /// Name of the first remote that produced this distinct repository entry, such as `origin` or `upstream`.
+ [JsonPropertyName("remoteName")]
+ public string RemoteName { get; set; } = string.Empty;
+}
+
+/// The GitHub repositories a working tree's remotes point at.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class GitReposFromRemotesResult
+{
+ /// One entry per distinct GitHub repository, in the order git reports the first remote for each repository. Empty when no remote points at a GitHub host, which a caller should read as `not connected to GitHub`. Failing to read the remotes is an error, not an empty list.
+ [JsonPropertyName("repositories")]
+ public IList Repositories { get => field ??= []; set; }
+}
+
+/// Git working tree whose GitHub remotes should be listed.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class GitReposFromRemotesRequest
+{
+ /// Absolute path to the root of the git working tree.
+ [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("gitRoot")]
+ public string GitRoot { get; set; } = string.Empty;
+}
+
/// Validated device-managed settings discovered before a session exists.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
public sealed class ManagedSettingsReadResult
@@ -8308,6 +8828,126 @@ internal sealed class SessionsEnrichMetadataRequest
public IList Sessions { get => field ??= []; set; }
}
+/// The workspace record that was written.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class SessionsCreateWorkspaceResult
+{
+ /// The created workspace record, as JSON.
+ [JsonPropertyName("workspaceJson")]
+ public string WorkspaceJson { get; set; } = string.Empty;
+}
+
+/// A working-directory context together with the client that produced it.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class SessionWorkingDirectoryContextWithClient
+{
+ /// Merge-base commit SHA.
+ [JsonPropertyName("baseCommit")]
+ public string? BaseCommit { get; set; }
+
+ /// Current git branch name.
+ [JsonPropertyName("branch")]
+ public string? Branch { get; set; }
+
+ /// Name of the client that created the session.
+ [JsonPropertyName("clientName")]
+ public string? ClientName { get; set; }
+
+ /// Current working directory path.
+ [JsonPropertyName("cwd")]
+ public string Cwd { get; set; } = string.Empty;
+
+ /// Root directory of the git repository.
+ [JsonPropertyName("gitRoot")]
+ public string? GitRoot { get; set; }
+
+ /// Head commit of the current git branch.
+ [JsonPropertyName("headCommit")]
+ public string? HeadCommit { get; set; }
+
+ /// Hosting platform type of the repository.
+ [JsonPropertyName("hostType")]
+ public string? HostType { get; set; }
+
+ /// Repository identifier derived from the git remote URL.
+ [JsonPropertyName("repository")]
+ public string? Repository { get; set; }
+
+ /// Raw host string from the git remote URL.
+ [JsonPropertyName("repositoryHost")]
+ public string? RepositoryHost { get; set; }
+}
+
+/// Identity, state location and starting context for a workspace record.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class SessionsCreateWorkspaceRequest
+{
+ /// Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`.
+ [JsonPropertyName("context")]
+ public SessionWorkingDirectoryContextWithClient? Context { get; set; }
+
+ /// `windows` (any letter case) selects Windows path rules. Any other value selects POSIX path rules.
+ [JsonPropertyName("convention")]
+ public string Convention { get; set; } = string.Empty;
+
+ /// User-supplied display name for the workspace.
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ /// Session ID the workspace record belongs to.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Directory the session's state is written under when no session filesystem provider is configured. Ignored when a provider is configured; the provider's session state path is used instead.
+ [JsonPropertyName("sessionStatePath")]
+ public string SessionStatePath { get; set; } = string.Empty;
+}
+
+/// The workspace record on disk, omitted when the session has none.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class SessionsLoadWorkspaceResult
+{
+ /// The workspace record, as JSON. Omitted when the record does not exist.
+ [JsonPropertyName("workspaceJson")]
+ public string? WorkspaceJson { get; set; }
+}
+
+/// Where the session's state lives, as a root directory and the session ID under it.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class SessionsLoadWorkspaceRequest
+{
+ /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Root directory every session's state directory sits under.
+ [JsonPropertyName("sessionsHome")]
+ public string SessionsHome { get; set; } = string.Empty;
+}
+
+/// The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class SessionsUpdateWorkspaceFieldsResult
+{
+}
+
+/// Where the session's state lives, plus workspace-schema fields to merge into its workspace record. Stored keys outside the schema are not preserved, and a stored `fork_count` is never replaced.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class SessionsUpdateWorkspaceFieldsRequest
+{
+ /// Workspace-schema fields to merge into the record, as a JSON object. Fields the object omits keep their stored values, except stored keys outside the schema are not preserved and a stored `fork_count` is never replaced.
+ [JsonPropertyName("fieldsJson")]
+ public string FieldsJson { get; set; } = string.Empty;
+
+ /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Root directory every session's state directory sits under.
+ [JsonPropertyName("sessionsHome")]
+ public string SessionsHome { get; set; } = string.Empty;
+}
+
/// Reload all hooks (user, plugin, optionally repo) and apply them to the active session. Call after installing or removing plugins so their hooks take effect immediately. No-op when no active session matches the given sessionId.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
public sealed class SessionsReloadPluginHooksResult
@@ -8355,47 +8995,6 @@ public sealed class SessionsSetAdditionalPluginsResult
{
}
-/// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source.
-[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
-public sealed class InstalledPlugin
-{
- /// Path where the plugin is cached locally.
- [JsonPropertyName("cache_path")]
- public string? CachePath { get; set; }
-
- /// Whether the plugin is currently enabled.
- [JsonPropertyName("enabled")]
- public bool Enabled { get; set; }
-
- /// Installation timestamp.
- [JsonPropertyName("installed_at")]
- public string InstalledAt { get; set; } = string.Empty;
-
- /// Absolute path of the marketplace directory a live plugin was resolved from. Present only on live, never-persisted records — those synthesized at session start for a directory/local marketplace, whose cache_path points at the real plugin directory on disk rather than a copy under the installed-plugins cache. Its presence is what marks a record as live, and no record carrying it is ever written to the persisted installedPlugins key.
- [JsonPropertyName("installed_from")]
- public string? InstalledFrom { get; set; }
-
- /// Marketplace the plugin came from (empty string for direct repo installs).
- [JsonPropertyName("marketplace")]
- public string Marketplace { get; set; } = string.Empty;
-
- /// Plugin name.
- [JsonPropertyName("name")]
- public string Name { get; set; } = string.Empty;
-
- /// Source for direct repo installs (when marketplace is empty).
- [JsonPropertyName("source")]
- public JsonElement? Source { get; set; }
-
- /// Per-plugin source fingerprint (a SHA-256 hash of the plugin's catalog source spec plus its resolved source subtree — NOT a Git commit SHA) captured at marketplace install/update time. Auto-update compares it against the freshly recomputed fingerprint to detect a content change that does not bump the version. Absent for pre-existing installs and for direct (non-marketplace) installs.
- [JsonPropertyName("source_sha")]
- public string? SourceSha { get; set; }
-
- /// Version installed (if available).
- [JsonPropertyName("version")]
- public string? Version { get; set; }
-}
-
/// Manager-wide additional plugins to register; replaces any previously-configured set.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
internal sealed class SessionsSetAdditionalPluginsRequest
@@ -8883,6 +9482,137 @@ internal sealed class AgentRegistrySpawnRequest
public AgentRegistrySpawnPermissionMode? PermissionMode { get; set; }
}
+/// Feature availability.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class ConnectorDiscoveryCapabilities
+{
+ /// API version.
+ [JsonPropertyName("apiVersion")]
+ public long ApiVersion { get; set; }
+
+ /// Availability.
+ [JsonPropertyName("availability")]
+ public ConnectorDiscoveryAvailability Availability { get; set; }
+
+ /// Whether results are cached.
+ [JsonPropertyName("conditionalCache")]
+ public bool ConditionalCache { get; set; }
+
+ /// Whether accounts are selected by opaque ID.
+ [JsonPropertyName("opaqueAccountSelection")]
+ public bool OpaqueAccountSelection { get; set; }
+}
+
+/// Account metadata.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class ConnectorDiscoveryAuthInfo
+{
+ /// Host.
+ [JsonPropertyName("host")]
+ public string Host { get; set; } = string.Empty;
+
+ /// Login.
+ [JsonPropertyName("login")]
+ public string Login { get; set; } = string.Empty;
+
+ /// Authentication type.
+ [JsonPropertyName("type")]
+ public AuthInfoType Type { get; set; }
+}
+
+/// Eligible account.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class ConnectorDiscoveryAccount
+{
+ /// Opaque account ID.
+ [JsonPropertyName("accountId")]
+ public string AccountId { get; set; } = string.Empty;
+
+ /// Account metadata.
+ [JsonPropertyName("authInfo")]
+ public ConnectorDiscoveryAuthInfo AuthInfo { get => field ??= new(); set; }
+}
+
+/// Eligible accounts.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class ConnectorDiscoveryAccountList
+{
+ /// Eligible accounts.
+ [JsonPropertyName("accounts")]
+ public IList Accounts { get => field ??= []; set; }
+
+ /// Availability.
+ [JsonPropertyName("availability")]
+ public ConnectorDiscoveryAvailability Availability { get; set; }
+}
+
+/// Entry.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class ConnectorDiscoveryCatalogEntry
+{
+ /// Description.
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+
+ /// Display name.
+ [JsonPropertyName("displayName")]
+ public string DisplayName { get; set; } = string.Empty;
+
+ /// Logo.
+ [JsonPropertyName("logo")]
+ public string? Logo { get; set; }
+
+ /// Name.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ /// Release tag.
+ [JsonPropertyName("releaseTag")]
+ public string? ReleaseTag { get; set; }
+
+ /// Status.
+ [JsonPropertyName("status")]
+ public ConnectorCatalogStatus Status { get; set; }
+
+ /// Tier.
+ [JsonPropertyName("tier")]
+ public string? Tier { get; set; }
+}
+
+/// Entries for the selected account.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class ConnectorDiscoveryCatalogResult
+{
+ /// Opaque account ID.
+ [JsonPropertyName("accountId")]
+ public string AccountId { get; set; } = string.Empty;
+
+ /// Entries.
+ [JsonPropertyName("connectors")]
+ public IList Connectors { get => field ??= []; set; }
+
+ /// Refresh time in Unix epoch milliseconds.
+ [JsonPropertyName("refreshedAtMs")]
+ public long RefreshedAtMs { get; set; }
+
+ /// Revision.
+ [JsonPropertyName("revision")]
+ public long Revision { get; set; }
+}
+
+/// Selected account.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class ConnectorDiscoveryAccountRequest
+{
+ /// Opaque account ID.
+ [RegularExpression("^\\S+$")]
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")]
+ [MinLength(1)]
+ [MaxLength(2048)]
+ [JsonPropertyName("accountId")]
+ public string AccountId { get; set; } = string.Empty;
+}
+
/// Identifies the target session.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
internal sealed class SessionSuspendRequest
@@ -10298,6 +11028,7 @@ public sealed class SessionSetCredentialsResult
[JsonDerivedType(typeof(SettableAuthInfoToken), "token")]
[JsonDerivedType(typeof(SettableAuthInfoCopilotApiToken), "copilot-api-token")]
[JsonDerivedType(typeof(SettableAuthInfoUser), "user")]
+[JsonDerivedType(typeof(SettableAuthInfoAccount), "account")]
[JsonDerivedType(typeof(SettableAuthInfoGhCli), "gh-cli")]
[JsonDerivedType(typeof(SettableAuthInfoApiKey), "api-key")]
public partial class SettableAuthInfo
@@ -10428,6 +11159,24 @@ public partial class SettableAuthInfoUser : SettableAuthInfo
public required string Login { get; set; }
}
+/// An interactive account whose model provider owns its credentials. It carries no GitHub credential.
+/// The account variant of .
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public partial class SettableAuthInfoAccount : SettableAuthInfo
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "account";
+
+ /// Host coordinate owned by the account's model provider.
+ [JsonPropertyName("host")]
+ public required string Host { get; set; }
+
+ /// Login identifying the provider-owned account.
+ [JsonPropertyName("login")]
+ public required string Login { get; set; }
+}
+
/// Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value.
/// The gh-cli variant of .
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
@@ -10804,6 +11553,11 @@ public partial class AuthReadValueActiveAccount : AuthReadValue
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("account")]
public AccountStatus? Account { get; set; }
+
+ /// Credential-free identity metadata for the active account, including resolved Copilot user information when available.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("authInfo")]
+ public AuthIdentity? AuthInfo { get; set; }
}
/// Neutral authentication status summary.
@@ -11073,10 +11827,35 @@ public partial class AuthLoginStepNeedsInteraction : AuthLoginStep
public override string Kind => "needs-interaction";
}
-/// Terminal result of an interactive login flow.
+/// A credential-free account choice after sign-in.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class AuthLoginAccount
+{
+ /// Host coordinate owned by the selected account's provider.
+ [JsonPropertyName("host")]
+ public string Host { get; set; } = string.Empty;
+
+ /// Provider kind that owns this account choice.
+ [JsonPropertyName("kind")]
+ public AccountKind Kind { get; set; }
+
+ /// Human-readable login for the account choice.
+ [JsonPropertyName("login")]
+ public string Login { get; set; } = string.Empty;
+
+ /// Opaque identifier supplied to the next login step to select this account.
+ [JsonPropertyName("selectionId")]
+ public string SelectionId { get; set; } = string.Empty;
+}
+
+/// Result of an interactive login flow. Pending consent or account selection is not terminal.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
public sealed class AuthLoginResultDto
{
+ /// Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 first.
+ [JsonPropertyName("accounts")]
+ public IList? Accounts { get; set; }
+
/// Host that was signed in, when completed.
[Url]
[StringSyntax(StringSyntaxAttribute.Uri)]
@@ -11087,7 +11866,7 @@ public sealed class AuthLoginResultDto
[JsonPropertyName("login")]
public string? Login { get; set; }
- /// Terminal disposition of the login.
+ /// Current disposition of the login, including pending user decisions.
[JsonPropertyName("status")]
public AuthLoginResultStatus Status { get; set; }
}
@@ -11100,7 +11879,7 @@ public partial class AuthLoginStepCompleted : AuthLoginStep
[JsonIgnore]
public override string Kind => "completed";
- /// The terminal login result.
+ /// Login result. When status is needs-plaintext-consent or needs-account-selection, advance with the user's decision to continue.
[JsonPropertyName("result")]
public required AuthLoginResultDto Result { get; set; }
}
@@ -13056,6 +13835,65 @@ internal sealed class ModelSetReasoningEffortRequest
public string SessionId { get; set; } = string.Empty;
}
+/// Availability of a server-advertised routing preference.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class AutoTierStatus
+{
+ /// Whether the provider permits selecting this preference.
+ [JsonPropertyName("enabled")]
+ public bool Enabled { get; set; }
+
+ /// Human-readable explanation of availability.
+ [JsonPropertyName("message")]
+ public string? Message { get; set; }
+
+ /// Extensible machine-readable unavailability reason.
+ [JsonPropertyName("reason")]
+ public string? Reason { get; set; }
+}
+
+/// A server-advertised routing preference. Identifiers and execution types are extensible.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class AutoTierDescriptor
+{
+ /// Description displayed beside the preference.
+ [JsonPropertyName("description")]
+ public string Description { get; set; } = string.Empty;
+
+ /// Human-readable label, not a routing identifier.
+ [JsonPropertyName("displayName")]
+ public string DisplayName { get; set; } = string.Empty;
+
+ /// Opaque routing identifier transmitted unchanged to the provider.
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+
+ /// Current account-specific availability.
+ [JsonPropertyName("status")]
+ public AutoTierStatus Status { get => field ??= new(); set; }
+
+ /// Execution kind; this client supports `auto` preferences on the Auto model.
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = string.Empty;
+}
+
+/// Account-bound discovery metadata for the virtual `auto` model.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class AutoTierMetadata
+{
+ /// Provider-default preference, used only when no explicit preference exists.
+ [JsonPropertyName("defaultTier")]
+ public string DefaultTier { get; set; } = string.Empty;
+
+ /// Provider that supplied this metadata, when the catalog is provider-attributed.
+ [JsonPropertyName("providerId")]
+ public string? ProviderId { get; set; }
+
+ /// Routing preferences in the server's presentation order.
+ [JsonPropertyName("tiers")]
+ public IList Tiers { get => field ??= []; set; }
+}
+
/// Cost-category metadata for a CAPI model.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
public sealed class SessionModelPriceCategory
@@ -13094,6 +13932,10 @@ public sealed class ModelProviderDescriptor
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
public sealed class SessionModelList
{
+ /// Ordered Auto routing preferences discovered for this session's account.
+ [JsonPropertyName("auto")]
+ public AutoTierMetadata? Auto { get; set; }
+
/// 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`).
[JsonPropertyName("list")]
public IList List { get => field ??= []; set; }
@@ -15485,6 +16327,32 @@ internal sealed class SessionSkillsEnsureLoadedRequest
public string SessionId { get; set; } = string.Empty;
}
+/// The IDE a host is connected to, as reported to the session.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class SessionConnectedIdeInfo
+{
+ /// Display name of the connected IDE, for example `VS Code`.
+ [JsonPropertyName("ideName")]
+ public string IdeName { get; set; } = string.Empty;
+
+ /// Absolute path of the workspace folder the IDE has open.
+ [JsonPropertyName("workspaceFolder")]
+ public string WorkspaceFolder { get; set; } = string.Empty;
+}
+
+/// Records which IDE the host is connected to, or clears it.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class SessionMcpSetConnectedIdeInfoParams
+{
+ /// The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports that it is disconnected.
+ [JsonPropertyName("ide")]
+ public SessionConnectedIdeInfo? Ide { get; set; }
+
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
/// Recorded MCP server connection failure.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
public sealed class McpServerFailureInfo
@@ -15591,6 +16459,10 @@ public sealed class McpServer
/// Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured.
[JsonPropertyName("status")]
public McpServerStatus Status { get; set; }
+
+ /// Configured URL for an HTTP/SSE server, regardless of configuration source. Omitted for local and in-memory servers.
+ [JsonPropertyName("url")]
+ public string? Url { get; set; }
}
/// MCP servers configured for the session, with their connection status and host-level state.
@@ -15615,6 +16487,73 @@ internal sealed class SessionMcpListRequest
public string SessionId { get; set; } = string.Empty;
}
+/// Observational state for a matching already materialized MCP server.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class McpConfiguredServerState
+{
+ /// Observed connection error, when the materialized server failed.
+ [JsonPropertyName("error")]
+ public string? Error { get; set; }
+
+ /// Observed connection status. This is not a configuration or readiness guarantee.
+ [JsonPropertyName("status")]
+ public McpServerStatus Status { get; set; }
+}
+
+/// Effective MCP configuration entry. Configuration enablement is distinct from the optional live observation.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class McpConfiguredServer
+{
+ /// Human-readable display name supplied by configuration.
+ [JsonPropertyName("displayName")]
+ public string? DisplayName { get; set; }
+
+ /// Whether this configured server is enabled after session configuration and policy filtering.
+ [JsonPropertyName("enabled")]
+ public bool Enabled { get; set; }
+
+ /// Observed state from an already materialized matching server. Omitted when no live graph has this configured server; it never determines configuration enablement.
+ [JsonPropertyName("live")]
+ public McpConfiguredServerState? Live { get; set; }
+
+ /// Server name (config key).
+ [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("name")]
+ public string Name { get; set; } = string.Empty;
+
+ /// Configuration provenance: user, workspace, plugin, builtin, or managed.
+ [JsonPropertyName("source")]
+ public McpServerSource? Source { get; set; }
+
+ /// Plugin name that provided this server, when source is plugin.
+ [JsonPropertyName("sourcePlugin")]
+ public string? SourcePlugin { get; set; }
+
+ /// Plugin version that provided this server, when source is plugin.
+ [JsonPropertyName("sourcePluginVersion")]
+ public string? SourcePluginVersion { get; set; }
+}
+
+/// Effective MCP configuration with optional live observations from matching already materialized servers.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class McpConfiguredServerList
+{
+ /// Effective configured MCP servers.
+ [JsonPropertyName("servers")]
+ public IList Servers { get => field ??= []; set; }
+}
+
+/// Identifies the target session.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+internal sealed class SessionMcpListConfiguredRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
/// Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
public sealed class McpToolUi
@@ -22949,7 +23888,7 @@ internal sealed class SessionMetadataActivityRequest
/// Token-usage breakdown for the session's current context window.
public sealed class MetadataContextInfoResultContextInfo
{
- /// Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%).
+ /// Output reservation overlapping the displayed prompt allowance plus tokens after the effective input budget's buffer-exhaustion blocking threshold (default 95%).
[JsonPropertyName("bufferTokens")]
public long BufferTokens { get; set; }
@@ -22961,7 +23900,7 @@ public sealed class MetadataContextInfoResultContextInfo
[JsonPropertyName("conversationTokens")]
public long ConversationTokens { get; set; }
- /// Prompt token limit plus the model's full output token limit.
+ /// Advertised prompt allowance for the selected context tier, without adding output tokens. The denominator for context-usage displays.
[JsonPropertyName("limit")]
public long Limit { get; set; }
@@ -22973,7 +23912,7 @@ public sealed class MetadataContextInfoResultContextInfo
[JsonPropertyName("modelName")]
public string ModelName { get; set; } = string.Empty;
- /// Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified).
+ /// Effective input budget: the selected tier's prompt allowance bounded by the combined context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when limits are unspecified.
[JsonPropertyName("promptTokenLimit")]
public long PromptTokenLimit { get; set; }
@@ -23003,11 +23942,11 @@ public sealed class MetadataContextInfoResult
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
internal sealed class MetadataContextInfoRequest
{
- /// Maximum output tokens allowed by the target model. Pass 0 if unknown.
+ /// Requested output allowance to reserve against the combined context ceiling. Pass 0 to resolve the session's request cap, falling back to the model's advertised output limit.
[JsonPropertyName("outputTokenLimit")]
public long OutputTokenLimit { get; set; }
- /// Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default.
+ /// Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from the session.
[JsonPropertyName("promptTokenLimit")]
public long PromptTokenLimit { get; set; }
@@ -23023,7 +23962,7 @@ internal sealed class MetadataContextInfoRequest
/// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`.
public sealed class MetadataContextAttributionResultContextAttributionCategories
{
- /// Output reserve plus post-blocking-threshold buffer.
+ /// Overlapping output reservation plus post-blocking-threshold buffer.
[JsonPropertyName("buffer")]
public long Buffer { get; set; }
@@ -23091,7 +24030,7 @@ public sealed class MetadataContextAttributionResultContextAttributionEntry
/// Per-source token attribution snapshot for the current context window. The heaviest individual messages are available separately via `metadata.getContextHeaviestMessages`.
public sealed class MetadataContextAttributionResultContextAttribution
{
- /// Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`.
+ /// Output reservation overlapping the displayed prompt allowance plus the tokens past the effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`.
[JsonPropertyName("bufferTokens")]
public long BufferTokens { get; set; }
@@ -23111,7 +24050,7 @@ public sealed class MetadataContextAttributionResultContextAttribution
[JsonPropertyName("entries")]
public IList Entries { get => field ??= []; set; }
- /// Prompt limit plus the model's output reserve: the full context window `categories.freeSpace` and `categories.buffer` are measured against. Mirrors `SessionContextInfo.limit`.
+ /// Advertised prompt allowance for the selected context tier: the denominator for context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. Mirrors `SessionContextInfo.limit`.
[JsonPropertyName("limit")]
public long Limit { get; set; }
@@ -23123,7 +24062,7 @@ public sealed class MetadataContextAttributionResultContextAttribution
[JsonPropertyName("modelSource")]
public string ModelSource { get; set; } = string.Empty;
- /// Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`.
+ /// Effective input budget after reserving requested output against the combined context ceiling. Mirrors `SessionContextInfo.promptTokenLimit`.
[JsonPropertyName("promptTokenLimit")]
public long PromptTokenLimit { get; set; }
@@ -23203,43 +24142,6 @@ public sealed class MetadataRecordContextChangeResult
{
}
-/// Updated working directory and git context. Emitted as the new payload of `session.context_changed`.
-[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
-public sealed class SessionWorkingDirectoryContext
-{
- /// Merge-base commit SHA (fork point from the remote default branch).
- [JsonPropertyName("baseCommit")]
- public string? BaseCommit { get; set; }
-
- /// Current git branch name.
- [JsonPropertyName("branch")]
- public string? Branch { get; set; }
-
- /// Current working directory path.
- [JsonPropertyName("cwd")]
- public string Cwd { get; set; } = string.Empty;
-
- /// Root directory of the git repository, resolved via git rev-parse.
- [JsonPropertyName("gitRoot")]
- public string? GitRoot { get; set; }
-
- /// Head commit of the current git branch.
- [JsonPropertyName("headCommit")]
- public string? HeadCommit { get; set; }
-
- /// Hosting platform type of the repository.
- [JsonPropertyName("hostType")]
- public SessionWorkingDirectoryContextHostType? HostType { get; set; }
-
- /// Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps).
- [JsonPropertyName("repository")]
- public string? Repository { get; set; }
-
- /// Raw host string from the git remote URL (e.g. "github.com", "dev.azure.com").
- [JsonPropertyName("repositoryHost")]
- public string? RepositoryHost { get; set; }
-}
-
/// Updated working-directory/git context to record on the session.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
internal sealed class MetadataRecordContextChangeRequest
@@ -31370,6 +32272,69 @@ public override void Write(Utf8JsonWriter writer, SlashCommandKind value, JsonSe
}
+/// Hosting platform type of the repository.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+[JsonConverter(typeof(Converter))]
+[DebuggerDisplay("{Value,nq}")]
+public readonly struct SessionWorkingDirectoryContextHostType : IEquatable
+{
+ private readonly string? _value;
+
+ /// Initializes a new instance of the struct.
+ /// The value to associate with this .
+ [JsonConstructor]
+ public SessionWorkingDirectoryContextHostType(string value)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(value);
+ _value = value;
+ }
+
+ /// Gets the value associated with this .
+ public string Value => _value ?? string.Empty;
+
+ /// The working directory repository is hosted on GitHub.
+ public static SessionWorkingDirectoryContextHostType GitHub { get; } = new("github");
+
+ /// The working directory repository is hosted on Azure DevOps.
+ public static SessionWorkingDirectoryContextHostType Ado { get; } = new("ado");
+
+ /// Returns a value indicating whether two instances are equivalent.
+ public static bool operator ==(SessionWorkingDirectoryContextHostType left, SessionWorkingDirectoryContextHostType right) => left.Equals(right);
+
+ /// Returns a value indicating whether two instances are not equivalent.
+ public static bool operator !=(SessionWorkingDirectoryContextHostType left, SessionWorkingDirectoryContextHostType right) => !(left == right);
+
+ ///
+ public override bool Equals(object? obj) => obj is SessionWorkingDirectoryContextHostType other && Equals(other);
+
+ ///
+ public bool Equals(SessionWorkingDirectoryContextHostType 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 SessionWorkingDirectoryContextHostType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert));
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, SessionWorkingDirectoryContextHostType value, JsonSerializerOptions options)
+ {
+ GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionWorkingDirectoryContextHostType));
+ }
+ }
+}
+
+
/// Severity of a managed-settings validation finding.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
[JsonConverter(typeof(Converter))]
@@ -32765,6 +33730,228 @@ public override void Write(Utf8JsonWriter writer, AgentRegistrySpawnPermissionMo
}
+/// Availability.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+[JsonConverter(typeof(Converter))]
+[DebuggerDisplay("{Value,nq}")]
+public readonly struct ConnectorDiscoveryAvailability : IEquatable
+{
+ private readonly string? _value;
+
+ /// Initializes a new instance of the struct.
+ /// The value to associate with this .
+ [JsonConstructor]
+ public ConnectorDiscoveryAvailability(string value)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(value);
+ _value = value;
+ }
+
+ /// Gets the value associated with this .
+ public string Value => _value ?? string.Empty;
+
+ /// Enabled.
+ public static ConnectorDiscoveryAvailability Enabled { get; } = new("enabled");
+
+ /// Disabled.
+ public static ConnectorDiscoveryAvailability Disabled { get; } = new("disabled");
+
+ /// Unavailable.
+ public static ConnectorDiscoveryAvailability Unavailable { get; } = new("unavailable");
+
+ /// Returns a value indicating whether two instances are equivalent.
+ public static bool operator ==(ConnectorDiscoveryAvailability left, ConnectorDiscoveryAvailability right) => left.Equals(right);
+
+ /// Returns a value indicating whether two instances are not equivalent.
+ public static bool operator !=(ConnectorDiscoveryAvailability left, ConnectorDiscoveryAvailability right) => !(left == right);
+
+ ///
+ public override bool Equals(object? obj) => obj is ConnectorDiscoveryAvailability other && Equals(other);
+
+ ///
+ public bool Equals(ConnectorDiscoveryAvailability 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 ConnectorDiscoveryAvailability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert));
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, ConnectorDiscoveryAvailability value, JsonSerializerOptions options)
+ {
+ GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorDiscoveryAvailability));
+ }
+ }
+}
+
+
+/// Authentication type.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+[JsonConverter(typeof(Converter))]
+[DebuggerDisplay("{Value,nq}")]
+public readonly struct AuthInfoType : IEquatable
+{
+ private readonly string? _value;
+
+ /// Initializes a new instance of the struct.
+ /// The value to associate with this .
+ [JsonConstructor]
+ public AuthInfoType(string value)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(value);
+ _value = value;
+ }
+
+ /// Gets the value associated with this .
+ public string Value => _value ?? string.Empty;
+
+ /// Authentication provided by a GitHub App HMAC credential.
+ public static AuthInfoType Hmac { get; } = new("hmac");
+
+ /// Authentication resolved from environment-provided credentials.
+ public static AuthInfoType Env { get; } = new("env");
+
+ /// Authentication from an interactive user sign-in.
+ public static AuthInfoType User { get; } = new("user");
+
+ /// Authentication from a selected provider-owned account, without a GitHub credential.
+ public static AuthInfoType Account { get; } = new("account");
+
+ /// Authentication delegated to the GitHub CLI.
+ public static AuthInfoType GhCli { get; } = new("gh-cli");
+
+ /// Authentication from an API key credential.
+ public static AuthInfoType ApiKey { get; } = new("api-key");
+
+ /// Authentication from a GitHub token.
+ public static AuthInfoType Token { get; } = new("token");
+
+ /// Authentication from an SDK GitHub token callback.
+ public static AuthInfoType TokenProvider { get; } = new("token-provider");
+
+ /// Authentication from a Copilot API token.
+ public static AuthInfoType CopilotApiToken { get; } = new("copilot-api-token");
+
+ /// Returns a value indicating whether two instances are equivalent.
+ public static bool operator ==(AuthInfoType left, AuthInfoType right) => left.Equals(right);
+
+ /// Returns a value indicating whether two instances are not equivalent.
+ public static bool operator !=(AuthInfoType left, AuthInfoType right) => !(left == right);
+
+ ///
+ public override bool Equals(object? obj) => obj is AuthInfoType other && Equals(other);
+
+ ///
+ public bool Equals(AuthInfoType 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 AuthInfoType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert));
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, AuthInfoType value, JsonSerializerOptions options)
+ {
+ GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AuthInfoType));
+ }
+ }
+}
+
+
+/// Authoritative service connection state for one Connector.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+[JsonConverter(typeof(Converter))]
+[DebuggerDisplay("{Value,nq}")]
+public readonly struct ConnectorCatalogStatus : IEquatable
+{
+ private readonly string? _value;
+
+ /// Initializes a new instance of the struct.
+ /// The value to associate with this .
+ [JsonConstructor]
+ public ConnectorCatalogStatus(string value)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(value);
+ _value = value;
+ }
+
+ /// Gets the value associated with this .
+ public string Value => _value ?? string.Empty;
+
+ /// The Connector is available but not connected.
+ public static ConnectorCatalogStatus NotConnected { get; } = new("not_connected");
+
+ /// The Connector service is still completing connection or consent.
+ public static ConnectorCatalogStatus Pending { get; } = new("pending");
+
+ /// The Connector is connected and may contribute MCP servers.
+ public static ConnectorCatalogStatus Connected { get; } = new("connected");
+
+ /// The Connector service reports an unusable connection.
+ public static ConnectorCatalogStatus Error { get; } = new("error");
+
+ /// The service returned a future or unrecognized state.
+ public static ConnectorCatalogStatus Unknown { get; } = new("unknown");
+
+ /// Returns a value indicating whether two instances are equivalent.
+ public static bool operator ==(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => left.Equals(right);
+
+ /// Returns a value indicating whether two instances are not equivalent.
+ public static bool operator !=(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => !(left == right);
+
+ ///
+ public override bool Equals(object? obj) => obj is ConnectorCatalogStatus other && Equals(other);
+
+ ///
+ public bool Equals(ConnectorCatalogStatus 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 ConnectorCatalogStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert));
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, ConnectorCatalogStatus value, JsonSerializerOptions options)
+ {
+ GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorCatalogStatus));
+ }
+ }
+}
+
+
/// The UI mode the agent was in when this message was sent. Defaults to the session's current mode.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
[JsonConverter(typeof(Converter))]
@@ -34217,87 +35404,6 @@ public override void Write(Utf8JsonWriter writer, PermissionDecisionSurface valu
}
-/// Authentication type.
-[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
-[JsonConverter(typeof(Converter))]
-[DebuggerDisplay("{Value,nq}")]
-public readonly struct AuthInfoType : IEquatable
-{
- private readonly string? _value;
-
- /// Initializes a new instance of the struct.
- /// The value to associate with this .
- [JsonConstructor]
- public AuthInfoType(string value)
- {
- ArgumentException.ThrowIfNullOrWhiteSpace(value);
- _value = value;
- }
-
- /// Gets the value associated with this .
- public string Value => _value ?? string.Empty;
-
- /// Authentication provided by a GitHub App HMAC credential.
- public static AuthInfoType Hmac { get; } = new("hmac");
-
- /// Authentication resolved from environment-provided credentials.
- public static AuthInfoType Env { get; } = new("env");
-
- /// Authentication from an interactive user sign-in.
- public static AuthInfoType User { get; } = new("user");
-
- /// Authentication delegated to the GitHub CLI.
- public static AuthInfoType GhCli { get; } = new("gh-cli");
-
- /// Authentication from an API key credential.
- public static AuthInfoType ApiKey { get; } = new("api-key");
-
- /// Authentication from a GitHub token.
- public static AuthInfoType Token { get; } = new("token");
-
- /// Authentication from an SDK GitHub token callback.
- public static AuthInfoType TokenProvider { get; } = new("token-provider");
-
- /// Authentication from a Copilot API token.
- public static AuthInfoType CopilotApiToken { get; } = new("copilot-api-token");
-
- /// Returns a value indicating whether two instances are equivalent.
- public static bool operator ==(AuthInfoType left, AuthInfoType right) => left.Equals(right);
-
- /// Returns a value indicating whether two instances are not equivalent.
- public static bool operator !=(AuthInfoType left, AuthInfoType right) => !(left == right);
-
- ///
- public override bool Equals(object? obj) => obj is AuthInfoType other && Equals(other);
-
- ///
- public bool Equals(AuthInfoType 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 AuthInfoType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert));
- }
-
- ///
- public override void Write(Utf8JsonWriter writer, AuthInfoType value, JsonSerializerOptions options)
- {
- GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AuthInfoType));
- }
- }
-}
-
-
/// The provider kind stamped on a signed-in account.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
[JsonConverter(typeof(Converter))]
@@ -34436,7 +35542,7 @@ public override void Write(Utf8JsonWriter writer, LoginProviderKind value, JsonS
}
-/// Terminal disposition of a login persistence attempt.
+/// Disposition of a login attempt, including pending user decisions.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
[JsonConverter(typeof(Converter))]
[DebuggerDisplay("{Value,nq}")]
@@ -34456,12 +35562,15 @@ public AuthLoginResultStatus(string value)
/// Gets the value associated with this .
public string Value => _value ?? string.Empty;
- /// The credential was persisted and the account is signed in.
+ /// The credential was persisted and the selected account is signed in.
public static AuthLoginResultStatus Completed { get; } = new("completed");
/// Persistence needs explicit consent to store the token in plaintext.
public static AuthLoginResultStatus NeedsPlaintextConsent { get; } = new("needs-plaintext-consent");
+ /// Credentials are saved; select an account using a returned selectionId as advance input to complete sign-in.
+ public static AuthLoginResultStatus NeedsAccountSelection { get; } = new("needs-account-selection");
+
/// The user declined plaintext persistence.
public static AuthLoginResultStatus Declined { get; } = new("declined");
@@ -37811,78 +38920,6 @@ public override void Write(Utf8JsonWriter writer, ConnectorAuthorizationScope va
}
-/// Authoritative service connection state for one Connector.
-[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
-[JsonConverter(typeof(Converter))]
-[DebuggerDisplay("{Value,nq}")]
-public readonly struct ConnectorCatalogStatus : IEquatable
-{
- private readonly string? _value;
-
- /// Initializes a new instance of the struct.
- /// The value to associate with this .
- [JsonConstructor]
- public ConnectorCatalogStatus(string value)
- {
- ArgumentException.ThrowIfNullOrWhiteSpace(value);
- _value = value;
- }
-
- /// Gets the value associated with this .
- public string Value => _value ?? string.Empty;
-
- /// The Connector is available but not connected.
- public static ConnectorCatalogStatus NotConnected { get; } = new("not_connected");
-
- /// The Connector service is still completing connection or consent.
- public static ConnectorCatalogStatus Pending { get; } = new("pending");
-
- /// The Connector is connected and may contribute MCP servers.
- public static ConnectorCatalogStatus Connected { get; } = new("connected");
-
- /// The Connector service reports an unusable connection.
- public static ConnectorCatalogStatus Error { get; } = new("error");
-
- /// The service returned a future or unrecognized state.
- public static ConnectorCatalogStatus Unknown { get; } = new("unknown");
-
- /// Returns a value indicating whether two instances are equivalent.
- public static bool operator ==(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => left.Equals(right);
-
- /// Returns a value indicating whether two instances are not equivalent.
- public static bool operator !=(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => !(left == right);
-
- ///
- public override bool Equals(object? obj) => obj is ConnectorCatalogStatus other && Equals(other);
-
- ///
- public bool Equals(ConnectorCatalogStatus 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 ConnectorCatalogStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert));
- }
-
- ///
- public override void Write(Utf8JsonWriter writer, ConnectorCatalogStatus value, JsonSerializerOptions options)
- {
- GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorCatalogStatus));
- }
- }
-}
-
-
/// Live MCP status of one Connector-owned runtime server.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
[JsonConverter(typeof(Converter))]
@@ -40268,69 +41305,6 @@ public override void Write(Utf8JsonWriter writer, WorkspaceSummaryHostType value
}
-/// Hosting platform type of the repository.
-[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
-[JsonConverter(typeof(Converter))]
-[DebuggerDisplay("{Value,nq}")]
-public readonly struct SessionWorkingDirectoryContextHostType : IEquatable
-{
- private readonly string? _value;
-
- /// Initializes a new instance of the struct.
- /// The value to associate with this .
- [JsonConstructor]
- public SessionWorkingDirectoryContextHostType(string value)
- {
- ArgumentException.ThrowIfNullOrWhiteSpace(value);
- _value = value;
- }
-
- /// Gets the value associated with this .
- public string Value => _value ?? string.Empty;
-
- /// The working directory repository is hosted on GitHub.
- public static SessionWorkingDirectoryContextHostType GitHub { get; } = new("github");
-
- /// The working directory repository is hosted on Azure DevOps.
- public static SessionWorkingDirectoryContextHostType Ado { get; } = new("ado");
-
- /// Returns a value indicating whether two instances are equivalent.
- public static bool operator ==(SessionWorkingDirectoryContextHostType left, SessionWorkingDirectoryContextHostType right) => left.Equals(right);
-
- /// Returns a value indicating whether two instances are not equivalent.
- public static bool operator !=(SessionWorkingDirectoryContextHostType left, SessionWorkingDirectoryContextHostType right) => !(left == right);
-
- ///
- public override bool Equals(object? obj) => obj is SessionWorkingDirectoryContextHostType other && Equals(other);
-
- ///
- public bool Equals(SessionWorkingDirectoryContextHostType 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 SessionWorkingDirectoryContextHostType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert));
- }
-
- ///
- public override void Write(Utf8JsonWriter writer, SessionWorkingDirectoryContextHostType value, JsonSerializerOptions options)
- {
- GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionWorkingDirectoryContextHostType));
- }
- }
-}
-
-
/// Rust-owned settings predicates exposed across the SDK boundary. Raw feature-flag names are intentionally not part of the contract.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
[JsonConverter(typeof(Converter))]
@@ -42080,6 +43054,12 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell
Interlocked.CompareExchange(ref field, new(_rpc), null) ??
field;
+ /// GlobalState APIs.
+ public ServerGlobalStateApi GlobalState =>
+ field ??
+ Interlocked.CompareExchange(ref field, new(_rpc), null) ??
+ field;
+
/// Commands APIs.
public ServerCommandsApi Commands =>
field ??
@@ -42092,6 +43072,24 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell
Interlocked.CompareExchange(ref field, new(_rpc), null) ??
field;
+ /// GitHubRepository APIs.
+ public ServerGitHubRepositoryApi GitHubRepository =>
+ field ??
+ Interlocked.CompareExchange(ref field, new(_rpc), null) ??
+ field;
+
+ /// GitHubOwners APIs.
+ public ServerGitHubOwnersApi GitHubOwners =>
+ field ??
+ Interlocked.CompareExchange(ref field, new(_rpc), null) ??
+ field;
+
+ /// Git APIs.
+ public ServerGitApi Git =>
+ field ??
+ Interlocked.CompareExchange(ref field, new(_rpc), null) ??
+ field;
+
/// ManagedSettings APIs.
public ServerManagedSettingsApi ManagedSettings =>
field ??
@@ -42127,6 +43125,12 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell
field ??
Interlocked.CompareExchange(ref field, new(_rpc), null) ??
field;
+
+ /// Connectors APIs.
+ public ServerConnectorsApi Connectors =>
+ field ??
+ Interlocked.CompareExchange(ref field, new(_rpc), null) ??
+ field;
}
/// Provides server-scoped Environments APIs.
@@ -43389,6 +44393,64 @@ public async Task GetDiscoveryPathsAsync(IList?
var request = new AgentsGetDiscoveryPathsRequest { ProjectPaths = projectPaths, ExcludeHostAgents = excludeHostAgents };
return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getDiscoveryPaths", [request], cancellationToken);
}
+
+ /// Lists the agents this runtime ships, by name. A consumer separating shipped agents from ones the user or a plugin authored should compare against these names rather than against `AgentInfo.source`: an authored agent may carry the `builtin` source while not being one of these, and the runtime treats the two as separate questions. `disableableNames` is the subset a user may turn off, which a client needs to decide whether to offer a toggle. `yamlBasedNames` is the subset backed by a shipped YAML definition, which a client needs before asking the runtime to load one.
+ /// The to monitor for cancellation requests. The default is .
+ /// The agents this runtime ships, named so a consumer can tell them apart from authored ones.
+ internal async Task GetBuiltinsAsync(CancellationToken cancellationToken = default)
+ {
+ return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getBuiltins", [], cancellationToken);
+ }
+
+ /// Lists the shipped agents a client should offer right now, filtered by the feature flags it passes. `getBuiltins` names every agent the runtime knows about; some of those are gated, so a client rendering a picker wants this narrower list together with the description to show beside each name.
+ /// Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or pass null for no flags.
+ /// Flag overrides keyed by name. A null entry uses the corresponding base flag; false explicitly disables it. Omit or pass null for no overrides.
+ /// The surface asking, which gates agents that only apply to one client. Omit or pass null to apply no client filter.
+ /// The to monitor for cancellation requests. The default is .
+ /// The shipped agents available under the requested flags.
+ internal async Task GetAvailableBuiltinsAsync(IDictionary? featureFlags = null, IDictionary? overrides = null, string? context = null, CancellationToken cancellationToken = default)
+ {
+ var request = new AgentsGetAvailableBuiltinsRequest { FeatureFlags = featureFlags, Overrides = overrides, Context = context };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getAvailableBuiltins", [request], cancellationToken);
+ }
+
+ /// Loads one shipped agent's YAML definition, for a client that needs what the agent declares rather than only its name. `getBuiltins` reports which names have a definition to load: a name outside its `yamlBasedNames` is special-cased in code and has none. The definition crosses as its own JSON rather than as contract-typed fields, because the runtime parses it with the agent schema's tolerant shape and re-typing it here would drop the keys that shape accepts and this one does not. The projected `__nativeCustomAgent` view the runtime derives is included, so a caller reading the declared model and a caller rendering the agent see the same definition.
+ /// The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside that list is special-cased in code and has no definition, and is reported as an error rather than as an empty definition.
+ /// The to monitor for cancellation requests. The default is .
+ /// One shipped agent's definition.
+ internal async Task GetBuiltinDefinitionAsync(string name, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(name);
+
+ var request = new AgentsGetBuiltinDefinitionRequest { Name = name };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getBuiltinDefinition", [request], cancellationToken);
+ }
+
+ /// Projects one shipped agent the way a picker lists it, reading only the metadata at the head of the definition file and stopping before the prompt body. `getBuiltinDefinition` answers the whole definition instead, so a client listing every shipped agent should prefer this one: the cost of a listing grows with the number of agents, and the prompt body is the part a listing never shows. The two also differ in shape. This returns the projected custom agent on its own, whereas `getBuiltinDefinition` returns the authored definition with that projection nested under `__nativeCustomAgent`.
+ /// The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here too, from its in-code definition.
+ /// The to monitor for cancellation requests. The default is .
+ /// One shipped agent, projected for a listing.
+ internal async Task GetBuiltinListingDefinitionAsync(string name, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(name);
+
+ var request = new AgentsGetBuiltinListingDefinitionRequest { Name = name };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getBuiltinListingDefinition", [request], cancellationToken);
+ }
+
+ /// Resolves the model a custom agent asks for against the models actually available, and answers both the model to switch to and the warning a user should see when the agent's preference cannot be met. A custom agent may name several acceptable models in preference order, so the decision is a match rather than a lookup, and an agent whose preference is unavailable is a normal outcome that produces a warning rather than an error. A host must call this rather than pick the first available name itself, because the preference order and the wording of the warning are what keep one installation's agent selection the same as another's.
+ /// The agent's declared `model:` entry, serialized. A single name or an ordered list of acceptable names.
+ /// The models available to this session, serialized in the shape the model list carries.
+ /// The to monitor for cancellation requests. The default is .
+ /// The model to switch to, and the warning to show when the agent's preference could not be met.
+ internal async Task CustomAgentInitialModelDecisionAsync(string agentModelsJson, string availableModelsJson, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(agentModelsJson);
+ ArgumentNullException.ThrowIfNull(availableModelsJson);
+
+ var request = new AgentsCustomAgentInitialModelDecisionParams { AgentModelsJson = agentModelsJson, AvailableModelsJson = availableModelsJson };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "agents.customAgentInitialModelDecision", [request], cancellationToken);
+ }
}
/// Provides server-scoped Instructions APIs.
@@ -43425,6 +44487,49 @@ public async Task GetDiscoveryPathsAsync(IListProvides server-scoped GlobalState APIs.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class ServerGlobalStateApi
+{
+ private readonly JsonRpc _rpc;
+
+ internal ServerGlobalStateApi(JsonRpc rpc)
+ {
+ _rpc = rpc;
+ }
+
+ /// Reads the host's machine-wide state: which plugins are installed and the one-off flags and timestamps that record what the user has already been shown or migrated. This is the state that outlives a single session and a single workspace, so a host reads it to decide whether to run a first-launch step, offer an onboarding prompt, or skip one it has already completed. The stored credentials are deliberately not part of this result; a caller that needs an authenticated identity asks the account methods for it instead. Reading is non-destructive and every field is optional, because a fresh install has recorded nothing yet.
+ /// The to monitor for cancellation requests. The default is .
+ /// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape.
+ internal async Task LoadAsync(CancellationToken cancellationToken = default)
+ {
+ return await CopilotClient.InvokeRpcAsync(_rpc, "globalState.load", [], cancellationToken);
+ }
+
+ /// Reads the host's machine-wide state exactly as `globalState.load` does, but from a caller-supplied configuration directory instead of the one the server resolved for itself. Use this when a consumer scopes a session to its own Copilot home — the SDK's per-session `configDir` override — so the state read matches the directory that session actually uses. An absent or empty `configDir` resolves the server's own home, making this identical to `globalState.load`. The stored credentials are omitted here for the same reason they are omitted from `globalState.load`: a caller that needs an authenticated identity asks the account methods instead, so pointing this at another directory cannot be used to read the credentials kept in it.
+ /// Copilot configuration directory to read the state document from, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to read the directory the server resolved for itself.
+ /// The to monitor for cancellation requests. The default is .
+ /// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape.
+ internal async Task LoadForConfigDirAsync(string? configDir = null, CancellationToken cancellationToken = default)
+ {
+ var request = new GlobalStateLoadForConfigDirRequest { ConfigDir = configDir };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "globalState.loadForConfigDir", [request], cancellationToken);
+ }
+
+ /// Records one top-level key in the host's machine-wide state, the counterpart to `globalState.load`. A host calls this to remember that it has shown an onboarding step, asked a one-off question, or completed a migration, so the next run can skip it. Only the named key is replaced and the rest of the document is preserved, which lets two writers record different flags without overwriting each other; passing no value removes the key instead. Only the keys a host records itself are writable: `appInstallNudgeResponded`, `appTipShown`, `askedSetupTerminals`, `autoFeedbackLastPromptedAt`, `firstLaunchAt`, `recentModelIds`, `sandboxCredentialProxyCaDeclined` and `sandboxOnboardingShown`. Every other key is refused, including `installedPlugins`, the stored credentials, `trustedFolders`, the staff flags and the signed-in accounts. Plugin enablement must use the plugin APIs, which apply repository and managed-policy checks.
+ /// Top-level key to write, named as it appears in the result of `globalState.load`. It must be one of the writable keys that `globalState.writeKey` lists.
+ /// Copilot configuration directory to write the state document in, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to write the directory the server resolved for itself. Mirrors `globalState.loadForConfigDir`, so a caller can read and write the same directory.
+ /// Value to store for the key. Omit it, or pass null, to remove the key instead.
+ /// The to monitor for cancellation requests. The default is .
+ internal async Task WriteKeyAsync(string key, string? configDir = null, object? value = null, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(key);
+
+ var request = new GlobalStateWriteKeyRequest { Key = key, ConfigDir = configDir, Value = CopilotClient.ToJsonElementForWire(value) };
+ await CopilotClient.InvokeRpcAsync(_rpc, "globalState.writeKey", [request], cancellationToken);
+ }
+}
+
/// Provides server-scoped Commands APIs.
[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
public sealed class ServerCommandsApi
@@ -43474,31 +44579,138 @@ internal ServerUserSettingsApi(JsonRpc rpc)
_rpc = rpc;
}
- /// Drops this runtime process's in-memory user settings cache so the next settings read observes disk.
+ /// Lists every known user setting from settings.json, each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time.
/// The to monitor for cancellation requests. The default is .
- public async Task ReloadAsync(CancellationToken cancellationToken = default)
- {
- await CopilotClient.InvokeRpcAsync(_rpc, "user.settings.reload", [], cancellationToken);
- }
-
- /// Lists every known user setting (settings.json overlaid with the legacy config.json, config.json wins), each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time.
- /// The to monitor for cancellation requests. The default is .
- /// Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides.
+ /// Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides.
public async Task GetAsync(CancellationToken cancellationToken = default)
{
return await CopilotClient.InvokeRpcAsync(_rpc, "user.settings.get", [], cancellationToken);
}
- /// Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. Returns the keys whose new value is shadowed by a legacy config.json entry (config.json wins on read), which the runtime leaves in place — such writes do not take effect until the legacy value is removed.
+ /// Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed.
/// Partial user settings to write, as a free-form object keyed by setting name.
/// The to monitor for cancellation requests. The default is .
- /// Outcome of writing user settings.
- public async Task SetAsync(object settings, CancellationToken cancellationToken = default)
+ public async Task SetAsync(object settings, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(settings);
var request = new UserSettingsSetRequest { Settings = CopilotClient.ToJsonElementForWire(settings)!.Value };
- return await CopilotClient.InvokeRpcAsync(_rpc, "user.settings.set", [request], cancellationToken);
+ await CopilotClient.InvokeRpcAsync(_rpc, "user.settings.set", [request], cancellationToken);
+ }
+}
+
+/// Provides server-scoped GitHubRepository APIs.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class ServerGitHubRepositoryApi
+{
+ private readonly JsonRpc _rpc;
+
+ internal ServerGitHubRepositoryApi(JsonRpc rpc)
+ {
+ _rpc = rpc;
+ }
+
+ /// Resolves the GitHub repository that owns a working-tree path by reading the selected git remote configured for it, preferring `origin`. Returns a null `repository` when the path is inside a git working tree but that selected remote does not resolve to a GitHub host. Fails when the path is not inside a git working tree at all, so a caller can tell 'not a repository' apart from 'a repository with no GitHub remote'.
+ /// Absolute path to a directory inside the git working tree to resolve.
+ /// The to monitor for cancellation requests. The default is .
+ /// The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host.
+ internal async Task AtPathAsync(string path, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(path);
+
+ var request = new GitHubRepositoryAtPathRequest { Path = path };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "gitHubRepository.atPath", [request], cancellationToken);
+ }
+}
+
+/// Provides server-scoped GitHubOwners APIs.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class ServerGitHubOwnersApi
+{
+ private readonly JsonRpc _rpc;
+
+ internal ServerGitHubOwnersApi(JsonRpc rpc)
+ {
+ _rpc = rpc;
+ }
+
+ /// Registers a cancellable owner listing and returns its request id. Separate from `gitHubOwners.list` so the id exists before the listing starts: a caller that abandons the listing the moment it begins would otherwise have nothing to name in `gitHubOwners.cancel`. The id serves one listing only. Long-abandoned unused ids can be released by later allocations.
+ /// The to monitor for cancellation requests. The default is .
+ /// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations.
+ internal async Task NextRequestIdAsync(CancellationToken cancellationToken = default)
+ {
+ return await CopilotClient.InvokeRpcAsync(_rpc, "gitHubOwners.nextRequestId", [], cancellationToken);
+ }
+
+ /// Lists the logins the authenticated user may act as — their own account first, then the organizations they belong to — by asking the GitHub API under the supplied credential. No credential travels in the request: `authInfo` selects one the runtime already holds, and the runtime resolves the token and the GitHub host from it. A failure the caller should render arrives as `message`; one it should raise arrives as `throwError`.
+ /// Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled before use, released after being abandoned, or already used is refused rather than silently running uncancellable.
+ /// The credential the listing runs under, carried opaquely because its shape is the host's own and the runtime only resolves a token and a GitHub host from it. No credential travels: this selects one the runtime already holds.
+ /// The to monitor for cancellation requests. The default is .
+ /// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render.
+ internal async Task ListAsync(long requestId, object authInfo, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(authInfo);
+
+ var request = new GitHubOwnersListRequest { RequestId = requestId, AuthInfo = CopilotClient.ToJsonElementForWire(authInfo)!.Value };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "gitHubOwners.list", [request], cancellationToken);
+ }
+
+ /// Abandons an owner listing started with the given request id. Answers `canceled: true` while a listing with that id is running. Answers `canceled: false` when the id was never registered, was registered but not used, was released after being abandoned, or its listing has ended. Canceling an unused id releases it, and a later `list` with that id is refused. The cancel acts only on owner listings and never reaches another request of the host.
+ /// Request id the listing was started with.
+ /// The to monitor for cancellation requests. The default is .
+ /// Whether the id named a running owner listing.
+ internal async Task CancelAsync(long requestId, CancellationToken cancellationToken = default)
+ {
+ var request = new GitHubOwnersCancelRequest { RequestId = requestId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "gitHubOwners.cancel", [request], cancellationToken);
+ }
+}
+
+/// Provides server-scoped Git APIs.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class ServerGitApi
+{
+ private readonly JsonRpc _rpc;
+
+ internal ServerGitApi(JsonRpc rpc)
+ {
+ _rpc = rpc;
+ }
+
+ /// Reads the remote that the branch checked out in a working tree tracks, as `branch.<name>.remote` configures it. Reports `origin` rather than failing whenever there is no tracking configuration to read — on a detached HEAD, on a branch with no upstream, or when git itself fails — because a caller asking which remote to talk to needs an answer it can act on, not an error. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on.
+ /// Absolute path to a directory inside the git working tree to query.
+ /// The to monitor for cancellation requests. The default is .
+ /// The remote the checked-out branch tracks.
+ internal async Task CurrentBranchRemoteAsync(string cwd, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(cwd);
+
+ var request = new GitCwdRequest { Cwd = cwd };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "git.currentBranchRemote", [request], cancellationToken);
+ }
+
+ /// Collects the repository context of a working directory in one call: working tree root, repository identifier and host, current branch, and the HEAD and base commits. Every repository field is omitted when the path is not inside a git working tree, and the requested path is echoed back as `cwd`. The answer is the same `SessionWorkingDirectoryContext` that `session.metadata.recordContextChange` accepts, so a caller polling for a context change can forward the result unchanged. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. It can become public once an SDK consumer needs to derive session context from a directory itself.
+ /// Absolute path to a directory inside the git working tree to query.
+ /// The to monitor for cancellation requests. The default is .
+ /// Updated working directory and git context. Emitted as the new payload of `session.context_changed`.
+ internal async Task WorkingDirectoryContextAsync(string cwd, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(cwd);
+
+ var request = new GitCwdRequest { Cwd = cwd };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "git.workingDirectoryContext", [request], cancellationToken);
+ }
+
+ /// Lists the GitHub repositories a working tree's remotes point at, one entry per distinct repository, so a caller can resolve a base and head repository without parsing remote URLs itself. When several remotes name the same repository, only the first is listed, and the entry keeps that remote name. Remotes pointing at no GitHub host are left out, so an empty list means the tree reaches GitHub through no remote. Failing to read the remotes is reported as an error rather than as an empty list, because the two mean different things to a caller. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on.
+ /// Absolute path to the root of the git working tree.
+ /// The to monitor for cancellation requests. The default is .
+ /// The GitHub repositories a working tree's remotes point at.
+ internal async Task ReposFromRemotesAsync(string gitRoot, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(gitRoot);
+
+ var request = new GitReposFromRemotesRequest { GitRoot = gitRoot };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "git.reposFromRemotes", [request], cancellationToken);
}
}
@@ -43946,6 +45158,54 @@ public async Task EnrichMetadataAsync(IList(_rpc, "sessions.enrichMetadata", [request], cancellationToken);
}
+ /// Creates the workspace record for a session that has not been opened yet. A host that hands a session off to another application — writing the record and then launching that application against the session ID — needs the record on disk before any session exists to carry it, which the session-scoped workspace methods cannot do. Replaces any existing record and resets the checkpoint index. When writing to the local filesystem, a stored `fork_count` survives on disk. Returns the record it built, so a surviving stored `fork_count` can differ from the answer.
+ /// Session ID the workspace record belongs to.
+ /// Directory the session's state is written under when no session filesystem provider is configured. Ignored when a provider is configured; the provider's session state path is used instead.
+ /// `windows` (any letter case) selects Windows path rules. Any other value selects POSIX path rules.
+ /// Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`.
+ /// User-supplied display name for the workspace.
+ /// The to monitor for cancellation requests. The default is .
+ /// The workspace record that was written.
+ internal async Task CreateWorkspaceAsync(string sessionId, string sessionStatePath, string convention, SessionWorkingDirectoryContextWithClient? context = null, string? name = null, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(sessionId);
+ ArgumentNullException.ThrowIfNull(sessionStatePath);
+ ArgumentNullException.ThrowIfNull(convention);
+
+ var request = new SessionsCreateWorkspaceRequest { SessionId = sessionId, SessionStatePath = sessionStatePath, Convention = convention, Context = context, Name = name };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.createWorkspace", [request], cancellationToken);
+ }
+
+ /// Reads a session's workspace record straight from disk, without opening the session. Resuming by session ID has to know where the session lives before it can connect, so the lookup cannot come from the session-scoped workspace methods, which resolve their location from a live session's context. Returns no record when the file is absent.
+ /// Root directory every session's state directory sits under.
+ /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home.
+ /// The to monitor for cancellation requests. The default is .
+ /// The workspace record on disk, omitted when the session has none.
+ internal async Task LoadWorkspaceAsync(string sessionsHome, string sessionId, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(sessionsHome);
+ ArgumentNullException.ThrowIfNull(sessionId);
+
+ var request = new SessionsLoadWorkspaceRequest { SessionsHome = sessionsHome, SessionId = sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.loadWorkspace", [request], cancellationToken);
+ }
+
+ /// Merges fields into a session's workspace record on disk, creating the record when it is absent. The counterpart to `sessions.loadWorkspace`, for the same before-the-session-exists case. It preserves stored workspace-schema fields the request does not supply, does not preserve stored keys outside the workspace schema, and never replaces a stored `fork_count`.
+ /// Root directory every session's state directory sits under.
+ /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home.
+ /// Workspace-schema fields to merge into the record, as a JSON object. Fields the object omits keep their stored values, except stored keys outside the schema are not preserved and a stored `fork_count` is never replaced.
+ /// The to monitor for cancellation requests. The default is .
+ /// The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays.
+ internal async Task UpdateWorkspaceFieldsAsync(string sessionsHome, string sessionId, string fieldsJson, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(sessionsHome);
+ ArgumentNullException.ThrowIfNull(sessionId);
+ ArgumentNullException.ThrowIfNull(fieldsJson);
+
+ var request = new SessionsUpdateWorkspaceFieldsRequest { SessionsHome = sessionsHome, SessionId = sessionId, FieldsJson = fieldsJson };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.updateWorkspaceFields", [request], cancellationToken);
+ }
+
/// Reloads user, plugin, and (optionally) repo hooks on the active session.
/// Active session ID to reload hooks for.
/// When true, skip repo-level hooks. Use before folder trust is confirmed; loadDeferredRepoHooks loads them post-trust.
@@ -44092,6 +45352,58 @@ public async Task SpawnAsync(string cwd, string? agent
}
}
+/// Provides server-scoped Connectors APIs.
+[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+public sealed class ServerConnectorsApi
+{
+ private readonly JsonRpc _rpc;
+
+ internal ServerConnectorsApi(JsonRpc rpc)
+ {
+ _rpc = rpc;
+ }
+
+ /// Returns feature availability.
+ /// The to monitor for cancellation requests. The default is .
+ /// Feature availability.
+ public async Task GetCapabilitiesAsync(CancellationToken cancellationToken = default)
+ {
+ return await CopilotClient.InvokeRpcAsync(_rpc, "connectors.getCapabilities", [], cancellationToken);
+ }
+
+ /// Returns eligible accounts.
+ /// The to monitor for cancellation requests. The default is .
+ /// Eligible accounts.
+ public async Task GetAccountsAsync(CancellationToken cancellationToken = default)
+ {
+ return await CopilotClient.InvokeRpcAsync(_rpc, "connectors.getAccounts", [], cancellationToken);
+ }
+
+ /// Lists entries for the selected account.
+ /// Opaque account ID.
+ /// The to monitor for cancellation requests. The default is .
+ /// Entries for the selected account.
+ public async Task ListAsync(string accountId, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(accountId);
+
+ var request = new ConnectorDiscoveryAccountRequest { AccountId = accountId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "connectors.list", [request], cancellationToken);
+ }
+
+ /// Refreshes entries for the selected account.
+ /// Opaque account ID.
+ /// The to monitor for cancellation requests. The default is .
+ /// Entries for the selected account.
+ public async Task RefreshAsync(string accountId, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(accountId);
+
+ var request = new ConnectorDiscoveryAccountRequest { AccountId = accountId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "connectors.refresh", [request], cancellationToken);
+ }
+}
+
/// Provides typed session-scoped RPC methods.
public sealed class SessionRpc
{
@@ -46402,7 +47714,18 @@ internal McpApi(CopilotSession session)
_session = 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.
+ /// Records the IDE the host is connected to, so the agent's system prompt can name it and its workspace folder. Null or an omitted `ide` clears the recorded value, which is how a host reports that it is disconnected; there is no separate clear method. Both `ideName` and `workspaceFolder` are required together, because half a state cannot be attributed to a project.
+ /// The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports that it is disconnected.
+ /// The to monitor for cancellation requests. The default is .
+ internal async Task SetConnectedIdeInfoAsync(SessionConnectedIdeInfo? ide = null, CancellationToken cancellationToken = default)
+ {
+ _session.ThrowIfDisposed();
+
+ var request = new SessionMcpSetConnectedIdeInfoParams { SessionId = _session.SessionId, Ide = ide };
+ await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.setConnectedIdeInfo", [request], cancellationToken);
+ }
+
+ /// Lists materialized MCP servers and their connection status. Cache misses may start and wait for MCP servers.
/// The to monitor for cancellation requests. The default is .
/// MCP servers configured for the session, with their connection status and host-level state.
public async Task ListAsync(CancellationToken cancellationToken = default)
@@ -46413,6 +47736,17 @@ public async Task ListAsync(CancellationToken cancellationToken =
return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.list", [request], cancellationToken);
}
+ /// Lists effective MCP configuration without starting, restarting, authenticating, or waiting for servers. An optional live observation is from an already materialized matching server; this is not a readiness guarantee.
+ /// The to monitor for cancellation requests. The default is .
+ /// Effective MCP configuration with optional live observations from matching already materialized servers.
+ public async Task ListConfiguredAsync(CancellationToken cancellationToken = default)
+ {
+ _session.ThrowIfDisposed();
+
+ var request = new SessionMcpListConfiguredRequest { SessionId = _session.SessionId };
+ return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.listConfigured", [request], cancellationToken);
+ }
+
/// 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 .
@@ -48658,8 +49992,8 @@ public async Task ActivityAsync(CancellationToken cancellationT
}
/// Returns the token breakdown for the session's current context window for a given model.
- /// Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default.
- /// Maximum output tokens allowed by the target model. Pass 0 if unknown.
+ /// Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from the session.
+ /// Requested output allowance to reserve against the combined context ceiling. Pass 0 to resolve the session's request cap, falling back to the model's advertised output limit.
/// Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values.
/// The to monitor for cancellation requests. The default is .
/// Token breakdown for the session's current context window, or null if uninitialized.
@@ -50536,13 +51870,23 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(AgentSelectRequest))]
[JsonSerializable(typeof(AgentSelectResult))]
[JsonSerializable(typeof(AgentSetPromptRequest))]
+[JsonSerializable(typeof(AgentsCustomAgentInitialModelDecisionParams))]
+[JsonSerializable(typeof(AgentsCustomAgentInitialModelDecisionResult))]
[JsonSerializable(typeof(AgentsDiscoverRequest))]
+[JsonSerializable(typeof(AgentsGetAvailableBuiltinsRequest))]
+[JsonSerializable(typeof(AgentsGetAvailableBuiltinsResult))]
+[JsonSerializable(typeof(AgentsGetBuiltinDefinitionRequest))]
+[JsonSerializable(typeof(AgentsGetBuiltinDefinitionResult))]
+[JsonSerializable(typeof(AgentsGetBuiltinListingDefinitionRequest))]
+[JsonSerializable(typeof(AgentsGetBuiltinListingDefinitionResult))]
+[JsonSerializable(typeof(AgentsGetBuiltinsResult))]
[JsonSerializable(typeof(AgentsGetDiscoveryPathsRequest))]
[JsonSerializable(typeof(AuthEnumerateQuery))]
[JsonSerializable(typeof(AuthEnumerateValue))]
[JsonSerializable(typeof(AuthIdentity))]
[JsonSerializable(typeof(AuthIdentityMetadata))]
[JsonSerializable(typeof(AuthInfo))]
+[JsonSerializable(typeof(AuthLoginAccount))]
[JsonSerializable(typeof(AuthLoginAdvanceRequest))]
[JsonSerializable(typeof(AuthLoginBeginRequest))]
[JsonSerializable(typeof(AuthLoginBegun))]
@@ -50555,11 +51899,15 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(AuthValidationError))]
[JsonSerializable(typeof(AuthWrite))]
[JsonSerializable(typeof(AuthWriteResult))]
+[JsonSerializable(typeof(AutoTierDescriptor))]
+[JsonSerializable(typeof(AutoTierMetadata))]
+[JsonSerializable(typeof(AutoTierStatus))]
[JsonSerializable(typeof(AutopilotObjectiveCreditLimit))]
[JsonSerializable(typeof(AutopilotObjectiveGetStateResult))]
[JsonSerializable(typeof(AutopilotObjectiveState))]
[JsonSerializable(typeof(BuiltInModelCatalog))]
[JsonSerializable(typeof(BuiltInModelCatalogEntry))]
+[JsonSerializable(typeof(BuiltinAgentSummary))]
[JsonSerializable(typeof(BuiltinToolDescriptor))]
[JsonSerializable(typeof(BuiltinToolFormat))]
[JsonSerializable(typeof(BuiltinToolInputSchema))]
@@ -50628,6 +51976,13 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(ConnectorConnectResult))]
[JsonSerializable(typeof(ConnectorContinueRequest))]
[JsonSerializable(typeof(ConnectorDisconnectResult))]
+[JsonSerializable(typeof(ConnectorDiscoveryAccount))]
+[JsonSerializable(typeof(ConnectorDiscoveryAccountList))]
+[JsonSerializable(typeof(ConnectorDiscoveryAccountRequest))]
+[JsonSerializable(typeof(ConnectorDiscoveryAuthInfo))]
+[JsonSerializable(typeof(ConnectorDiscoveryCapabilities))]
+[JsonSerializable(typeof(ConnectorDiscoveryCatalogEntry))]
+[JsonSerializable(typeof(ConnectorDiscoveryCatalogResult))]
[JsonSerializable(typeof(ConnectorReconcileRequest))]
[JsonSerializable(typeof(ConnectorReconcileRequestWithSession))]
[JsonSerializable(typeof(ConnectorRuntimeStatus))]
@@ -50702,12 +52057,29 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(FolderTrustAddParams))]
[JsonSerializable(typeof(FolderTrustCheckParams))]
[JsonSerializable(typeof(FolderTrustCheckResult))]
+[JsonSerializable(typeof(GitCurrentBranchRemoteResult))]
+[JsonSerializable(typeof(GitCwdRequest))]
[JsonSerializable(typeof(GitHubEnvironment))]
+[JsonSerializable(typeof(GitHubOwnerOption))]
+[JsonSerializable(typeof(GitHubOwnersCancelRequest))]
+[JsonSerializable(typeof(GitHubOwnersCancelResult))]
+[JsonSerializable(typeof(GitHubOwnersListRequest))]
+[JsonSerializable(typeof(GitHubOwnersListResult))]
+[JsonSerializable(typeof(GitHubOwnersRequestIdResult))]
+[JsonSerializable(typeof(GitHubRepositoryAtPathRequest))]
+[JsonSerializable(typeof(GitHubRepositoryAtPathResult))]
+[JsonSerializable(typeof(GitHubRepositoryIdentity))]
[JsonSerializable(typeof(GitHubTelemetryClientInfo))]
[JsonSerializable(typeof(GitHubTelemetryEvent))]
[JsonSerializable(typeof(GitHubTelemetryNotification))]
[JsonSerializable(typeof(GitHubTokenAcquireRequest))]
[JsonSerializable(typeof(GitHubTokenAcquireResult))]
+[JsonSerializable(typeof(GitRemoteRepository))]
+[JsonSerializable(typeof(GitReposFromRemotesRequest))]
+[JsonSerializable(typeof(GitReposFromRemotesResult))]
+[JsonSerializable(typeof(GlobalStateLoadForConfigDirRequest))]
+[JsonSerializable(typeof(GlobalStateLoadResult))]
+[JsonSerializable(typeof(GlobalStateWriteKeyRequest))]
[JsonSerializable(typeof(HandlePendingToolCallRequest))]
[JsonSerializable(typeof(HandlePendingToolCallResult))]
[JsonSerializable(typeof(HistoryAbortManualCompactionResult))]
@@ -50781,6 +52153,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(LocalSessionMetadataValue))]
[JsonSerializable(typeof(LogRequest))]
[JsonSerializable(typeof(LogResult))]
+[JsonSerializable(typeof(LoggedInUser))]
[JsonSerializable(typeof(LspInitializeRequest))]
[JsonSerializable(typeof(ManagedSettingMeta))]
[JsonSerializable(typeof(ManagedSettingsComposeLayer))]
@@ -50832,6 +52205,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(McpConfigUpdateRequest))]
[JsonSerializable(typeof(McpConfigureGitHubRequest))]
[JsonSerializable(typeof(McpConfigureGitHubResult))]
+[JsonSerializable(typeof(McpConfiguredServer))]
+[JsonSerializable(typeof(McpConfiguredServerList))]
+[JsonSerializable(typeof(McpConfiguredServerState))]
[JsonSerializable(typeof(McpDiagnosticDetails))]
[JsonSerializable(typeof(McpDiagnosticSourceConfiguration))]
[JsonSerializable(typeof(McpDisableRequest))]
@@ -51244,6 +52620,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(SessionCommandsListRequestWithSession))]
[JsonSerializable(typeof(SessionCompletionItem))]
[JsonSerializable(typeof(SessionCompletionsGetTriggerCharactersRequest))]
+[JsonSerializable(typeof(SessionConnectedIdeInfo))]
[JsonSerializable(typeof(SessionConnectorsGetAccountRequest))]
[JsonSerializable(typeof(SessionConnectorsGetCapabilitiesRequest))]
[JsonSerializable(typeof(SessionConnectorsGetStatusRequest))]
@@ -51312,6 +52689,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(SessionLoadDeferredRepoHooksResult))]
[JsonSerializable(typeof(SessionManagedSettingsGetRequest))]
[JsonSerializable(typeof(SessionMcpAppsGetHostContextRequest))]
+[JsonSerializable(typeof(SessionMcpListConfiguredRequest))]
[JsonSerializable(typeof(SessionMcpListRequest))]
[JsonSerializable(typeof(SessionMcpMoveLoadingToBackgroundRequest))]
[JsonSerializable(typeof(SessionMcpOauthCancelLoginRequest))]
@@ -51320,6 +52698,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(SessionMcpOauthPrepareLoginResult))]
[JsonSerializable(typeof(SessionMcpReloadRequest))]
[JsonSerializable(typeof(SessionMcpRemoveGitHubRequest))]
+[JsonSerializable(typeof(SessionMcpSetConnectedIdeInfoParams))]
[JsonSerializable(typeof(SessionMetadataActivityRequest))]
[JsonSerializable(typeof(SessionMetadataGetClientMetadataRequest))]
[JsonSerializable(typeof(SessionMetadataGetContextAttributionRequest))]
@@ -51398,6 +52777,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(SessionVisibilityGetRequest))]
[JsonSerializable(typeof(SessionWorkflowPauseAtCheckpointResult))]
[JsonSerializable(typeof(SessionWorkingDirectoryContext))]
+[JsonSerializable(typeof(SessionWorkingDirectoryContextWithClient))]
[JsonSerializable(typeof(SessionWorkspacesAutopilotObjectiveExistsRequest))]
[JsonSerializable(typeof(SessionWorkspacesDeleteAutopilotObjectiveRequest))]
[JsonSerializable(typeof(SessionWorkspacesGetWorkspaceRequest))]
@@ -51410,6 +52790,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(SessionsClientMetadataEntry))]
[JsonSerializable(typeof(SessionsCloseRequest))]
[JsonSerializable(typeof(SessionsCloseResult))]
+[JsonSerializable(typeof(SessionsCreateWorkspaceRequest))]
+[JsonSerializable(typeof(SessionsCreateWorkspaceResult))]
[JsonSerializable(typeof(SessionsDeleteRequest))]
[JsonSerializable(typeof(SessionsEnrichMetadataRequest))]
[JsonSerializable(typeof(SessionsFindByPrefixRequest))]
@@ -51433,6 +52815,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(SessionsListNonEmptySessionIdsResult))]
[JsonSerializable(typeof(SessionsListRequest))]
[JsonSerializable(typeof(SessionsLoadDeferredRepoHooksRequest))]
+[JsonSerializable(typeof(SessionsLoadWorkspaceRequest))]
+[JsonSerializable(typeof(SessionsLoadWorkspaceResult))]
[JsonSerializable(typeof(SessionsOpenProgress))]
[JsonSerializable(typeof(SessionsPruneOldRequest))]
[JsonSerializable(typeof(SessionsReadPersistedEventsRequest))]
@@ -51448,6 +52832,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(SessionsStartRemoteControlRequest))]
[JsonSerializable(typeof(SessionsStopRemoteControlRequest))]
[JsonSerializable(typeof(SessionsTransferRemoteControlRequest))]
+[JsonSerializable(typeof(SessionsUpdateWorkspaceFieldsRequest))]
+[JsonSerializable(typeof(SessionsUpdateWorkspaceFieldsResult))]
[JsonSerializable(typeof(SettableAuthInfo))]
[JsonSerializable(typeof(ShellCancelUserRequestedRequest))]
[JsonSerializable(typeof(ShellCredentials))]
@@ -51580,7 +52966,6 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(UserSettingMetadata))]
[JsonSerializable(typeof(UserSettingsGetResult))]
[JsonSerializable(typeof(UserSettingsSetRequest))]
-[JsonSerializable(typeof(UserSettingsSetResult))]
[JsonSerializable(typeof(VisibilityGetResult))]
[JsonSerializable(typeof(VisibilitySetRequest))]
[JsonSerializable(typeof(VisibilitySetResult))]
diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs
index c482a8a69f..1c621ace9d 100644
--- a/dotnet/src/Generated/SessionEvents.cs
+++ b/dotnet/src/Generated/SessionEvents.cs
@@ -5351,6 +5351,11 @@ public sealed partial class ModelCallFailureData
[JsonPropertyName("reasoningEffort")]
public string? ReasoningEffort { get; set; }
+ /// Serialized (uncompressed) byte length of the failed request body. A content-free size signal.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("requestBodyBytes")]
+ public long? RequestBodyBytes { get; set; }
+
/// Content-free structural summary of the failing request. Contains only counts and shape flags (no prompt content), so it is safe for unrestricted telemetry. Populated only for client-error (4xx) failures.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("requestFingerprint")]
@@ -13261,7 +13266,7 @@ public sealed partial class McpAppToolCallCompleteToolMeta
public McpAppToolCallCompleteToolMetaUI? Ui { get; set; }
}
-/// Routing preference used when the session model is `auto`. `fast` is an integrator-only latency preset and is not a first-party GitHub Copilot product preference.
+/// Extensible routing preference for the virtual `auto` model. New identifiers must be advertised and enabled by the provider. `fast` is an integrator-only latency preset.
[JsonConverter(typeof(Converter))]
[DebuggerDisplay("{Value,nq}")]
public readonly struct AutoTier : IEquatable
@@ -13302,10 +13307,10 @@ public AutoTier(string value)
public override bool Equals(object? obj) => obj is AutoTier other && Equals(other);
///
- public bool Equals(AutoTier other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase);
+ public bool Equals(AutoTier other) => string.Equals(Value, other.Value, StringComparison.Ordinal);
///
- public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
+ public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value);
///
public override string ToString() => Value;
@@ -14406,7 +14411,7 @@ public override void Write(Utf8JsonWriter writer, ModelDeselectedReason value, J
}
}
-/// Auto preferences that Copilot API can recommend.
+/// Enabled Auto preferences that Copilot API can recommend.
[JsonConverter(typeof(Converter))]
[DebuggerDisplay("{Value,nq}")]
public readonly struct RecommendedAutoTier : IEquatable
@@ -14444,10 +14449,10 @@ public RecommendedAutoTier(string value)
public override bool Equals(object? obj) => obj is RecommendedAutoTier other && Equals(other);
///
- public bool Equals(RecommendedAutoTier other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase);
+ public bool Equals(RecommendedAutoTier other) => string.Equals(Value, other.Value, StringComparison.Ordinal);
///
- public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
+ public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value);
///
public override string ToString() => Value;
@@ -17271,7 +17276,7 @@ public override void Write(Utf8JsonWriter writer, AbortReason value, JsonSeriali
}
}
-/// Configuration source: user, workspace, plugin, builtin, or managed.
+/// Configuration source: user, workspace, plugin, builtin, managed, or account.
[JsonConverter(typeof(Converter))]
[DebuggerDisplay("{Value,nq}")]
public readonly struct McpServerSource : IEquatable
@@ -17305,6 +17310,9 @@ public McpServerSource(string value)
/// Server supplied by a trusted host-managed catalog.
public static McpServerSource Managed { get; } = new("managed");
+ /// Server contributed by a signed-in account; enablement and organization policy still apply.
+ public static McpServerSource Account { get; } = new("account");
+
/// Returns a value indicating whether two instances are equivalent.
public static bool operator ==(McpServerSource left, McpServerSource right) => left.Equals(right);
@@ -19634,6 +19642,9 @@ public PermissionApprovalEvaluationReasonCode(string value)
/// The script path was not authorized for inspection.
public static PermissionApprovalEvaluationReasonCode PathNotAuthorized { get; } = new("path-not-authorized");
+ /// A code source was excluded from review by content exclusion policy.
+ public static PermissionApprovalEvaluationReasonCode ContentExcluded { get; } = new("content-excluded");
+
/// The script working directory was invalid.
public static PermissionApprovalEvaluationReasonCode InvalidWorkingDirectory { get; } = new("invalid-working-directory");
@@ -19670,6 +19681,24 @@ public PermissionApprovalEvaluationReasonCode(string value)
/// The script argument binding could not be reviewed.
public static PermissionApprovalEvaluationReasonCode ArgumentBindingUnreviewable { get; } = new("argument-binding-unreviewable");
+ /// The shell command could not be analyzed for execution evidence.
+ public static PermissionApprovalEvaluationReasonCode UnsupportedCommandShape { get; } = new("unsupported-command-shape");
+
+ /// The shell command used a code source that cannot be bound for review.
+ public static PermissionApprovalEvaluationReasonCode UnsupportedSource { get; } = new("unsupported-source");
+
+ /// The shell command used a code source computed at run time.
+ public static PermissionApprovalEvaluationReasonCode DynamicSource { get; } = new("dynamic-source");
+
+ /// The shell command referenced more code sources than can be reviewed.
+ public static PermissionApprovalEvaluationReasonCode TooManySources { get; } = new("too-many-sources");
+
+ /// A code-bearing executable could not be inspected.
+ public static PermissionApprovalEvaluationReasonCode ExecutableUnavailable { get; } = new("executable-unavailable");
+
+ /// A code-bearing executable exceeded the binding size limit.
+ public static PermissionApprovalEvaluationReasonCode ExecutableTooLarge { get; } = new("executable-too-large");
+
/// The script review metadata was malformed.
public static PermissionApprovalEvaluationReasonCode MalformedScriptActionReview { get; } = new("malformed-script-action-review");
@@ -21471,7 +21500,7 @@ public McpServerStatus(string value)
/// The server is configured but disabled.
public static McpServerStatus Disabled { get; } = new("disabled");
- /// The server was intentionally stopped and can be restarted on demand when policy permits; a server quarantined by restrictive managed policy stays stopped and cannot be restarted until the policy allows it.
+ /// The server is not running: it may not have started yet, may have been explicitly stopped, or may be quarantined by restrictive managed policy. It can be restarted on demand when policy permits.
public static McpServerStatus Stopped { get; } = new("stopped");
/// The server is not configured for this session.
diff --git a/dotnet/src/JsonRpc.cs b/dotnet/src/JsonRpc.cs
index 26d4fe297a..8d45eb6f4a 100644
--- a/dotnet/src/JsonRpc.cs
+++ b/dotnet/src/JsonRpc.cs
@@ -1039,11 +1039,12 @@ private sealed class IncomingRequestCancellation : IDisposable
public IncomingRequestCancellation(long id, CancellationToken connectionClosedToken)
{
Id = id;
- _combinedSource = CancellationTokenSource.CreateLinkedTokenSource(_requestSource.Token, connectionClosedToken);
_registration = _requestSource.Token.Register(static state =>
{
((TaskCompletionSource)state!).TrySetResult();
}, _requestCancelled);
+ // Cancellation callbacks run in reverse registration order: propagate to handlers before replying.
+ _combinedSource = CancellationTokenSource.CreateLinkedTokenSource(_requestSource.Token, connectionClosedToken);
}
public long Id { get; }
diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs
index f0cdde8999..88d00cde24 100644
--- a/dotnet/src/Session.cs
+++ b/dotnet/src/Session.cs
@@ -73,6 +73,7 @@ public sealed partial class CopilotSession : IAsyncDisposable
private volatile Func>? _elicitationHandler;
private volatile Func>? _exitPlanModeHandler;
private volatile Func>? _autoModeSwitchHandler;
+ private volatile ISkillProvider? _skillProvider;
private ImmutableArray _eventHandlers = ImmutableArray.Empty;
private sealed record EventSubscription(Type EventType, Action Handler, bool RootAgentOnly);
@@ -233,6 +234,7 @@ internal void CloseEventChannel()
///
internal void Unregister()
{
+ ClearSkillProvider();
CancelPendingExternalTools();
CloseEventChannel();
RemoveFromClient();
@@ -693,9 +695,9 @@ private static Dictionary ToJsonElementDictionary(IDictiona
/// are the same as the tools supplied when creating or resuming a session.
///
///
- /// Tool handlers switch after the runtime accepts the replacement. Tool calls already running finish with the handlers
- /// that started them. If the runtime rejects the replacement, the previous handlers remain installed and the exception is
- /// propagated. Concurrent calls are applied in order.
+ /// Tool handlers switch when the runtime's acceptance response arrives, before subsequent tool requests are dispatched.
+ /// Tool calls already running finish with the handlers that started them. If the runtime rejects the replacement, the
+ /// previous handlers remain installed and the exception is propagated. Concurrent calls are applied in order.
///
///
/// The agent sees the new tools from its next model request, which can fall within a turn in progress. A model request
@@ -714,9 +716,8 @@ public async Task SetToolsAsync(ICollection tools, Cancel
var wireTools = tools.Select(ToProtocolExternalToolDefinition).ToList();
var handlers = BuildToolHandlerMap(tools);
- // Cancelling while an earlier call holds the lock sends nothing. Once this call holds it, the
- // request runs to completion even if the caller stops waiting, so an accepted replacement still
- // installs its handlers.
+ // Cancelling before sending the request leaves handlers unchanged. Once sent, the request runs
+ // to completion even if the caller stops waiting, so an accepted replacement still installs its handlers.
var replacement = ReplaceToolsAsync(wireTools, handlers, cancellationToken);
try
{
@@ -741,8 +742,13 @@ private async Task ReplaceToolsAsync(
await _setToolsLock.WaitAsync(lockCancellationToken);
try
{
- await Rpc.Tools.SetAsync(wireTools, CancellationToken.None);
- Volatile.Write(ref _toolHandlers, handlers);
+ // SemaphoreSlim can grant a released slot while its cancellation continuation is pending.
+ lockCancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ var request = new ToolsSetRequest { SessionId = SessionId, Tools = wireTools };
+ await CopilotClient.InvokeRpcAsync(
+ JsonRpc, "session.tools.set", [request], null, CancellationToken.None,
+ onResponseInline: _ => Volatile.Write(ref _toolHandlers, handlers));
}
finally
{
@@ -1402,6 +1408,48 @@ internal void RegisterAutoModeSwitchHandler(Func
+ /// Registers the session-scoped skill provider callback.
+ ///
+ internal void RegisterSkillProvider(ISkillProvider? provider)
+ {
+ _skillProvider = provider;
+ }
+
+ internal void ClearSkillProvider() => _skillProvider = null;
+
+ internal async ValueTask HandleSkillProviderListAsync(CancellationToken cancellationToken)
+ {
+ var provider = _skillProvider ?? throw new InvalidOperationException($"No skill provider for session: {SessionId}");
+
+ try
+ {
+ var skills = await provider.ListSkillsAsync(cancellationToken).ConfigureAwait(false);
+ return new CopilotClient.SkillProviderListResult(skills?.ToList() ?? []);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested)
+ {
+ LogSkillProviderFailed(ex, "listSkills", SessionId);
+ throw new InvalidOperationException("Skill provider listSkills failed", ex);
+ }
+ }
+
+ internal async ValueTask HandleSkillProviderReadAsync(string name, CancellationToken cancellationToken)
+ {
+ var provider = _skillProvider ?? throw new InvalidOperationException($"No skill provider for session: {SessionId}");
+
+ try
+ {
+ var markdown = await provider.ReadSkillAsync(name, cancellationToken).ConfigureAwait(false);
+ return new CopilotClient.SkillProviderReadResult(markdown);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested)
+ {
+ LogSkillProviderFailed(ex, "readSkill", SessionId);
+ throw new InvalidOperationException("Skill provider readSkill failed", ex);
+ }
+ }
+
///
/// Registers per-provider BearerTokenProvider callbacks for BYOK
/// providers configured with managed-identity / on-demand bearer-token auth.
@@ -2392,6 +2440,7 @@ public async ValueTask DisposeAsync()
return;
}
+ ClearSkillProvider();
CancelPendingExternalTools();
CloseEventChannel();
@@ -2428,6 +2477,7 @@ public async ValueTask DisposeAsync()
_elicitationHandler = null;
_exitPlanModeHandler = null;
_autoModeSwitchHandler = null;
+ _skillProvider = null;
}
[LoggerMessage(Level = LogLevel.Error, Message = "Unhandled exception in broadcast event handler")]
@@ -2445,6 +2495,9 @@ public async ValueTask DisposeAsync()
[LoggerMessage(Level = LogLevel.Error, Message = "Permission handler or response delivery failed. SessionId={SessionId}, RequestId={RequestId}")]
private partial void LogPermissionHandlerOrDeliveryFailed(Exception exception, string sessionId, string requestId);
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Skill provider {Operation} failed. SessionId={SessionId}")]
+ private partial void LogSkillProviderFailed(Exception exception, string operation, string sessionId);
+
internal record SendMessageRequest
{
public string SessionId { get; init; } = string.Empty;
diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs
index 7ed04c279b..82a291e400 100644
--- a/dotnet/src/Types.cs
+++ b/dotnet/src/Types.cs
@@ -52,6 +52,74 @@ internal static class Diagnostics
internal const string Experimental = "GHCP001";
}
+///
+/// Describes a skill supplied by a session-scoped .
+///
+///
+/// Experimental. The runtime validates descriptor limits, name syntax, and
+/// case-insensitive uniqueness when it reads the provider catalog.
+///
+[Experimental(Diagnostics.Experimental)]
+public sealed class SkillProviderDescriptor
+{
+ /// Stable skill name.
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ /// Human-readable description of what the skill teaches the model.
+ [JsonPropertyName("description")]
+ public required string Description { get; set; }
+
+ ///
+ /// Whether users can invoke the skill explicitly. When unset, the runtime default is .
+ ///
+ [JsonPropertyName("userInvocable")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? UserInvocable { get; set; }
+
+ ///
+ /// Whether model-initiated invocation is disabled. When unset, the runtime default is .
+ ///
+ [JsonPropertyName("disableModelInvocation")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? DisableModelInvocation { get; set; }
+
+ /// Optional argument hint shown for explicit invocations.
+ [JsonPropertyName("argumentHint")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? ArgumentHint { get; set; }
+}
+
+///
+/// Provides session-scoped skills to the Copilot runtime.
+///
+///
+/// Experimental. Implementations can be called concurrently and must be safe for
+/// concurrent use. The provider is not persisted; supply it again when resuming a
+/// session. Returning from
+/// reports that the requested skill was not found.
+///
+[Experimental(Diagnostics.Experimental)]
+public interface ISkillProvider
+{
+ /// Lists the skills currently available from this provider.
+ ///
+ /// Cancels when the runtime abandons the request (for example, on timeout, session disposal,
+ /// or provider replacement on resume) or the connection closes.
+ ///
+ /// The available skill descriptors.
+ Task> ListSkillsAsync(CancellationToken cancellationToken);
+
+ /// Reads the SKILL.md markdown for a named skill.
+ /// The skill name requested by the runtime.
+ ///
+ /// Cancels when the runtime abandons the request (for example, on timeout, session disposal,
+ /// or provider replacement on resume) or the connection closes.
+ ///
+ /// The skill markdown, or when the skill is not found.
+ Task ReadSkillAsync(string name, CancellationToken cancellationToken);
+}
+
///
/// Log level for the Copilot runtime. Use the well-known values exposed as
/// static members (, , ,
@@ -3434,6 +3502,15 @@ public sealed class ManagedSettingsPermissions
/// Tool-permission patterns that are allowed without prompting.
[JsonPropertyName("allow")]
public IList? Allow { get; set; }
+
+ ///
+ /// Closed-world host boundary expressed as Domain(hostname),
+ /// Domain(IP), or Domain(*.example.com) rules. Schemes, ports,
+ /// paths, queries, and fragments are rejected. Multiple managed layers
+ /// intersect their lists. A present empty list denies all hosts.
+ ///
+ [JsonPropertyName("limitTo")]
+ public IList? LimitTo { get; set; }
}
///
@@ -3572,6 +3649,7 @@ protected SessionConfigBase(SessionConfigBase? other)
ExpAssignments = other.ExpAssignments;
EnableManagedSettings = other.EnableManagedSettings;
ManagedSettings = other.ManagedSettings;
+ SkillProvider = other.SkillProvider;
#pragma warning disable GHCP001
Canvases = other.Canvases is not null ? [.. other.Canvases] : null;
RequestCanvasRenderer = other.RequestCanvasRenderer;
@@ -3714,6 +3792,20 @@ protected SessionConfigBase(SessionConfigBase? other)
///
public bool? EnableSkills { get; set; }
+ ///
+ /// Session-scoped skill provider. When set, the runtime can list and read
+ /// skills by calling back into this SDK host.
+ ///
+ ///
+ /// Experimental. The provider is ephemeral and is never persisted; supply it
+ /// again when resuming a session. In ,
+ /// set to to make provider
+ /// skills active. Providers can be called concurrently.
+ ///
+ [JsonIgnore]
+ [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)]
+ public ISkillProvider? SkillProvider { get; set; }
+
///
/// Built-in skill names to include in the session. In
/// , omitting this option excludes all
@@ -4877,6 +4969,7 @@ public sealed class SystemMessageTransformRpcResponse
[JsonSerializable(typeof(SectionOverride))]
[JsonSerializable(typeof(SessionMetadata))]
[JsonSerializable(typeof(SetForegroundSessionResponse))]
+[JsonSerializable(typeof(SkillProviderDescriptor))]
[JsonSerializable(typeof(SystemMessageConfig))]
[JsonSerializable(typeof(ToolBinaryResult))]
[JsonSerializable(typeof(ToolBinaryResultType))]
diff --git a/dotnet/test/E2E/ScenarioTestingProvidersE2ETests.cs b/dotnet/test/E2E/ScenarioTestingProvidersE2ETests.cs
index 13270d3e43..da8f0eccf2 100644
--- a/dotnet/test/E2E/ScenarioTestingProvidersE2ETests.cs
+++ b/dotnet/test/E2E/ScenarioTestingProvidersE2ETests.cs
@@ -281,7 +281,7 @@ public async Task Should_Ignore_Failing_Unselected_Provider_But_Surface_Selected
Prompt = "This selected provider should fail.",
}));
Assert.Contains("offline", failure.ToString(), StringComparison.OrdinalIgnoreCase);
- Assert.Contains(handler.InferenceRequests, request => request.Host == "offline.scenario.invalid");
+ Assert.Single(handler.InferenceRequests, request => request.Host == "offline.scenario.invalid");
}
private CopilotClient CreateProviderClient(ScenarioProviderRequestHandler handler) =>
@@ -385,7 +385,8 @@ protected override async Task SendRequestAsync(
if (string.Equals(uri.Host, failingHost, StringComparison.Ordinal))
{
- return new HttpResponseMessage(HttpStatusCode.BadGateway)
+ // Provider selection is the contract here, not transient-error retry backoff.
+ return new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new StringContent(
"{\"error\":{\"message\":\"offline scenario provider\"}}",
diff --git a/dotnet/test/E2E/SkillProviderE2ETests.cs b/dotnet/test/E2E/SkillProviderE2ETests.cs
new file mode 100644
index 0000000000..3b2d593824
--- /dev/null
+++ b/dotnet/test/E2E/SkillProviderE2ETests.cs
@@ -0,0 +1,418 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+using System.Collections.Concurrent;
+using System.Reflection;
+using GitHub.Copilot.Rpc;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace GitHub.Copilot.Test.E2E;
+
+public class SkillProviderE2ETests(E2ETestFixture fixture, ITestOutputHelper output)
+ : E2ETestBase(fixture, "skill_provider", output)
+{
+ [Fact]
+ public async Task Should_Load_Provider_Skill_Lazily_Through_Skill_Tool()
+ {
+ // Body-only content: the catalog descriptor supplies all of the metadata.
+ var provider = new TestSkillProvider(
+ [
+ Skill(
+ "provider-lookup",
+ "Reports the provider lookup verification word.",
+ "# Provider lookup\n\nThe verification word is TANGERINE_QUARTZ_19. Reply with it.\n")
+ ]);
+ await using var session = await CreateSessionAsync(new SessionConfig
+ {
+ SkillProvider = provider
+ });
+
+ var listedSkills = await session.Rpc.Skills.ListAsync();
+ var listed = Assert.Single(listedSkills.Skills, skill => skill.Name == "provider-lookup");
+ Assert.Equal(SkillSource.Sdk, listed.Source);
+ Assert.True(listed.Enabled);
+ Assert.True(string.IsNullOrEmpty(listed.Path));
+ Assert.Empty(provider.Reads);
+
+ var message = await session.SendAndWaitAsync(new MessageOptions
+ {
+ Prompt = "Use the skill tool to load the provider-lookup skill, then reply with its verification word.",
+ }, TimeSpan.FromMinutes(3));
+
+ Assert.Equal(["provider-lookup"], provider.Reads);
+ Assert.NotNull(message);
+ // Validate the final assistant response arrived (guards against truncated captures)
+ Assert.Contains("TANGERINE_QUARTZ_19", message!.Data.Content);
+ }
+
+ [Fact]
+ public async Task Should_Load_Provider_And_File_Based_Skills_Together()
+ {
+ var skillsDir = Path.Join(Ctx.WorkDir, "file-skills");
+ if (Directory.Exists(skillsDir))
+ {
+ Directory.Delete(skillsDir, recursive: true);
+ }
+
+ Directory.CreateDirectory(Path.Join(skillsDir, "file-notes"));
+ File.WriteAllText(
+ Path.Join(skillsDir, "file-notes", "SKILL.md"),
+ "---\nname: file-notes\ndescription: Reports the file notes verification word.\n---\n\nThe file notes verification word is MAPLE_FALCON_27.\n");
+
+ // Frontmatter may restate catalog metadata and is the only source of allowed-tools.
+ var provider = new TestSkillProvider(
+ [
+ Skill(
+ "provider-audit",
+ "Reports the provider audit verification word.",
+ "---\nname: provider-audit\nallowed-tools: view\n---\n\nThe provider audit verification word is COBALT_HERON_58.\n")
+ ]);
+ await using var session = await CreateSessionAsync(new SessionConfig
+ {
+ SkillDirectories = [skillsDir],
+ SkillProvider = provider
+ });
+
+ var listedSkills = await session.Rpc.Skills.ListAsync();
+ var fileSkill = Assert.Single(listedSkills.Skills, skill => skill.Name == "file-notes");
+ var providerSkill = Assert.Single(listedSkills.Skills, skill => skill.Name == "provider-audit");
+ Assert.NotEqual(SkillSource.Sdk, fileSkill.Source);
+ Assert.False(string.IsNullOrWhiteSpace(fileSkill.Path));
+ Assert.Equal(SkillSource.Sdk, providerSkill.Source);
+
+ var message = await session.SendAndWaitAsync(new MessageOptions
+ {
+ Prompt = "Use the skill tool to load the file-notes skill and the provider-audit skill, then reply with both verification words.",
+ }, TimeSpan.FromMinutes(3));
+
+ Assert.Equal(["provider-audit"], provider.Reads);
+ Assert.NotNull(message);
+ Assert.Contains("MAPLE_FALCON_27", message!.Data.Content);
+ // Validate the final assistant response arrived (guards against truncated captures)
+ Assert.Contains("COBALT_HERON_58", message.Data.Content);
+ }
+
+ [Fact]
+ public async Task Should_Rebind_Skill_Provider_On_Resume()
+ {
+ var original = new TestSkillProvider(
+ [
+ Skill(
+ "rebind-check",
+ "Reports the rebind verification word.",
+ "The rebind verification word is AMBER_ALPHA_11.\n")
+ ]);
+ var replacement = new TestSkillProvider(
+ [
+ Skill(
+ "rebind-check",
+ "Reports the rebind verification word.",
+ "The rebind verification word is BRONZE_BETA_22.\n")
+ ]);
+ var first = await CreateSessionAsync(new SessionConfig
+ {
+ SkillProvider = original
+ });
+ var sessionId = first.SessionId;
+ var ready = await first.SendAndWaitAsync(new MessageOptions
+ {
+ Prompt = "Without using any tools or skills, reply with exactly REBIND_READY.",
+ }, TimeSpan.FromMinutes(3));
+ Assert.NotNull(ready);
+ Assert.Contains("REBIND_READY", ready!.Data.Content);
+
+ await first.DisposeAsync();
+ Assert.Empty(original.Reads);
+ var originalCallsBeforeResume = original.Calls.Count;
+
+ await using var session = await ResumeSessionAsync(sessionId, new ResumeSessionConfig
+ {
+ SkillProvider = replacement
+ });
+
+ var message = await session.SendAndWaitAsync(new MessageOptions
+ {
+ Prompt = "Use the skill tool to load the rebind-check skill, then reply with its verification word.",
+ }, TimeSpan.FromMinutes(3));
+
+ Assert.Equal(["rebind-check"], replacement.Reads);
+ Assert.Equal(originalCallsBeforeResume, original.Calls.Count);
+ Assert.NotNull(message);
+ // Validate the final assistant response arrived (guards against truncated captures)
+ Assert.Contains("BRONZE_BETA_22", message!.Data.Content);
+ Assert.DoesNotContain("AMBER_ALPHA_11", message.Data.Content);
+ }
+
+ [Fact]
+ public async Task Should_Report_Provider_Read_Failure_Without_Leaking_Details()
+ {
+ const string secret = "PROVIDER_SECRET_7F3A9C";
+ var provider = new TestSkillProvider(
+ [
+ new ProvidedSkill(
+ new SkillProviderDescriptor
+ {
+ Name = "broken-lookup",
+ Description = "Reports the broken lookup verification word.",
+ },
+ () => throw new InvalidOperationException($"database unavailable: {secret}"))
+ ]);
+ var events = new ConcurrentQueue();
+ await using var session = await CreateSessionAsync(new SessionConfig
+ {
+ SkillProvider = provider,
+ OnEvent = events.Enqueue
+ });
+
+ var message = await session.SendAndWaitAsync(new MessageOptions
+ {
+ Prompt = "Use the skill tool to load the broken-lookup skill. If loading fails, reply with exactly LOAD_FAILED.",
+ }, TimeSpan.FromMinutes(3));
+
+ Assert.Contains("broken-lookup", provider.Reads);
+ var failures = events
+ .OfType()
+ .Where(evt => !evt.Data.Success)
+ .ToArray();
+ var failure = Assert.Single(failures);
+ Assert.DoesNotContain(secret, string.Join("\n", events.Select(evt => evt.ToJson())), StringComparison.Ordinal);
+ Assert.NotNull(failure.Data.Error);
+ Assert.NotNull(message);
+ // Validate the final assistant response arrived (guards against truncated captures)
+ Assert.Contains("LOAD_FAILED", message!.Data.Content);
+ }
+
+ [Fact]
+ public async Task Should_Report_Missing_Provider_Skill_As_Not_Found()
+ {
+ var provider = new TestSkillProvider(
+ [
+ new ProvidedSkill(
+ new SkillProviderDescriptor
+ {
+ Name = "vanished-lookup",
+ Description = "Reports the vanished lookup verification word.",
+ },
+ () => null)
+ ]);
+ var events = new ConcurrentQueue();
+ await using var session = await CreateSessionAsync(new SessionConfig
+ {
+ SkillProvider = provider,
+ OnEvent = events.Enqueue
+ });
+
+ var message = await session.SendAndWaitAsync(new MessageOptions
+ {
+ Prompt = "Use the skill tool to load the vanished-lookup skill. If loading fails, reply with exactly LOAD_FAILED.",
+ }, TimeSpan.FromMinutes(3));
+
+ Assert.Contains("vanished-lookup", provider.Reads);
+ var failures = events
+ .OfType()
+ .Where(evt => !evt.Data.Success)
+ .ToArray();
+ var failure = Assert.Single(failures);
+ Assert.Contains("not found", failure.ToJson(), StringComparison.OrdinalIgnoreCase);
+ Assert.NotNull(message);
+ // Validate the final assistant response arrived (guards against truncated captures)
+ Assert.Contains("LOAD_FAILED", message!.Data.Content);
+ }
+
+ [Fact]
+ public async Task Should_Keep_Provider_Dormant_When_Skills_Disabled()
+ {
+ var provider = new TestSkillProvider(
+ [
+ Skill("dormant-lookup", "Never listed.", "Never read.\n")
+ ]);
+ await using var session = await CreateSessionAsync(new SessionConfig
+ {
+ EnableSkills = false,
+ SkillProvider = provider
+ });
+
+ await session.Rpc.Skills.EnsureLoadedAsync();
+ var listedSkills = await session.Rpc.Skills.ListAsync();
+
+ Assert.DoesNotContain(listedSkills.Skills, skill => skill.Source == SkillSource.Sdk);
+ Assert.Empty(provider.Calls);
+ }
+
+ [Fact]
+ public async Task Should_Unbind_Provider_When_Resumed_Without_One()
+ {
+ var provider = new TestSkillProvider(
+ [
+ Skill("unbound-lookup", "Reports the unbound lookup word.", "Unbound.\n")
+ ]);
+ var first = await CreateSessionAsync(new SessionConfig
+ {
+ SkillProvider = provider
+ });
+ var before = await first.Rpc.Skills.ListAsync();
+ Assert.Contains(before.Skills, skill => skill.Name == "unbound-lookup");
+ var callsBeforeResume = provider.Calls.Count;
+
+ UntrackSessionWithoutDetach(first);
+
+ CopilotSession? session = null;
+ try
+ {
+ session = await Ctx.ResumeSessionAsync(Client, first.SessionId, new ResumeSessionConfig
+ {
+ OnPermissionRequest = PermissionHandler.ApproveAll,
+ });
+
+ await session.Rpc.Skills.ReloadAsync();
+ var listedSkills = await session.Rpc.Skills.ListAsync();
+
+ Assert.DoesNotContain(listedSkills.Skills, skill => skill.Source == SkillSource.Sdk);
+ Assert.Equal(callsBeforeResume, provider.Calls.Count);
+ }
+ finally
+ {
+ if (session is not null)
+ {
+ await session.DisposeAsync();
+ }
+
+ await first.DisposeAsync();
+ }
+ }
+
+ [Fact]
+ public async Task Should_Cancel_A_Blocked_Provider_Call_When_The_Session_Is_Disposed()
+ {
+ var provider = new BlockingSkillProvider();
+ var session = await CreateSessionAsync(new SessionConfig
+ {
+ SkillProvider = provider
+ });
+
+ // The list RPC may fail or omit provider skills once the binding is removed;
+ // only the provider's cancellation matters here.
+ var list = session.Rpc.Skills.ListAsync();
+ await provider.Entered.WaitAsync(TimeSpan.FromSeconds(30));
+
+ await session.DisposeAsync();
+
+ await provider.Cancelled.WaitAsync(TimeSpan.FromSeconds(10));
+ await Record.ExceptionAsync(() => list);
+ }
+
+ [Fact]
+ public async Task Should_Reject_Skill_Provider_For_Cloud_Sessions()
+ {
+ var provider = new TestSkillProvider(
+ [
+ Skill("cloud-lookup", "Never listed.", "Never read.\n")
+ ]);
+
+ var exception = await Assert.ThrowsAsync(() =>
+ Client.CreateSessionAsync(new SessionConfig
+ {
+ Cloud = new CloudSessionOptions(),
+ SkillProvider = provider,
+ }));
+
+ Assert.Equal("Skill providers are not supported for cloud sessions.", exception.Message);
+ Assert.Empty(provider.Calls);
+ }
+
+ private static ProvidedSkill Skill(string name, string description, string markdown) =>
+ new(
+ new SkillProviderDescriptor
+ {
+ Name = name,
+ Description = description,
+ },
+ () => markdown);
+
+ private static void UntrackSessionWithoutDetach(CopilotSession session)
+ {
+ // Match a warm runtime resume: remove the SDK wrapper so resume is allowed, but do not detach the runtime session.
+ var removeFromClient = typeof(CopilotSession).GetMethod(
+ "RemoveFromClient",
+ BindingFlags.Instance | BindingFlags.NonPublic)
+ ?? throw new InvalidOperationException("CopilotSession.RemoveFromClient was not found.");
+ removeFromClient.Invoke(session, null);
+ }
+
+ private sealed record ProvidedSkill(SkillProviderDescriptor Descriptor, Func Read);
+
+ private sealed class BlockingSkillProvider : ISkillProvider
+ {
+ private readonly TaskCompletionSource _entered = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ private readonly TaskCompletionSource _cancelled = new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ public Task Entered => _entered.Task;
+
+ public Task Cancelled => _cancelled.Task;
+
+ public async Task> ListSkillsAsync(CancellationToken cancellationToken)
+ {
+ _entered.TrySetResult();
+ try
+ {
+ await Task.Delay(Timeout.Infinite, cancellationToken);
+ }
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+ // Not a token callback: if cancellation resumes this method inline, disposing
+ // a registration whose callback has not run yet drops it.
+ _cancelled.TrySetResult();
+ throw;
+ }
+
+ return [];
+ }
+
+ public Task ReadSkillAsync(string name, CancellationToken cancellationToken) =>
+ Task.FromResult(null);
+ }
+
+ private sealed class TestSkillProvider(IReadOnlyList skills) : ISkillProvider
+ {
+ private readonly object _lock = new();
+ private readonly List _calls = [];
+
+ public IReadOnlyList Calls
+ {
+ get
+ {
+ lock (_lock)
+ {
+ return [.. _calls];
+ }
+ }
+ }
+
+ public IReadOnlyList Reads => Calls
+ .Where(call => call.StartsWith("read:", StringComparison.Ordinal))
+ .Select(call => call["read:".Length..])
+ .ToArray();
+
+ public Task> ListSkillsAsync(CancellationToken cancellationToken)
+ {
+ AddCall("list");
+ return Task.FromResult>(skills.Select(skill => skill.Descriptor).ToArray());
+ }
+
+ public Task ReadSkillAsync(string name, CancellationToken cancellationToken)
+ {
+ AddCall($"read:{name}");
+ return Task.FromResult(skills.FirstOrDefault(skill => skill.Descriptor.Name == name)?.Read());
+ }
+
+ private void AddCall(string call)
+ {
+ lock (_lock)
+ {
+ _calls.Add(call);
+ }
+ }
+ }
+}
diff --git a/dotnet/test/E2E/StructuredOutputE2ETests.cs b/dotnet/test/E2E/StructuredOutputE2ETests.cs
index b18e93b492..e82e31d008 100644
--- a/dotnet/test/E2E/StructuredOutputE2ETests.cs
+++ b/dotnet/test/E2E/StructuredOutputE2ETests.cs
@@ -392,6 +392,15 @@ public async Task Rejects_Unsupported_Or_Oversized_Schemas_Before_Admission()
{
var config = StructuredSessionConfig();
config.Model = model;
+ config.EnableExperimentalMode = model == "hydrafusion";
+ if (model == "hydrafusion")
+ {
+ config.FeatureFlags = new Dictionary
+ {
+ ["HYDRAFUSION"] = true,
+ ["HYDRAFUSION_ROLLOUT"] = true,
+ };
+ }
config.OnPermissionRequest = PermissionHandler.ApproveAll;
await using var session = await Ctx.CreateSessionAsync(client, config);
await Assert.ThrowsAsync(() => session.SendAndWaitAsync(
diff --git a/dotnet/test/Unit/AutoTierIdentityTests.cs b/dotnet/test/Unit/AutoTierIdentityTests.cs
new file mode 100644
index 0000000000..9ca10e346d
--- /dev/null
+++ b/dotnet/test/Unit/AutoTierIdentityTests.cs
@@ -0,0 +1,46 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+using System.Text.Json;
+using System.Text.Json.Serialization.Metadata;
+using GitHub.Copilot.Rpc;
+using Xunit;
+
+#pragma warning disable GHCP001 // Exercise the public model RPC projection.
+
+namespace GitHub.Copilot.Test.Unit;
+
+public class AutoTierIdentityTests
+{
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
+ };
+
+ [Fact]
+ public void CaseDistinctTierIdsRemainDistinctAcrossCollectionsAndJson()
+ {
+ var custom = new AutoTier("BALANCE");
+ Assert.NotEqual(AutoTier.Balance, custom);
+ Assert.Equal(2, new HashSet { AutoTier.Balance, custom }.Count);
+ Assert.Equal("\"BALANCE\"", JsonSerializer.Serialize(custom, JsonOptions));
+ Assert.Equal(custom, JsonSerializer.Deserialize("\"BALANCE\"", JsonOptions));
+ Assert.Equal("balance", AutoTier.Balance.Value);
+
+ var rpc = JsonSerializer.Deserialize("""{"modelId":"auto","autoTier":"BALANCE"}""", JsonOptions);
+ Assert.NotNull(rpc);
+ Assert.Equal(custom, rpc.AutoTier);
+ Assert.NotEqual(AutoTier.Balance, rpc.AutoTier);
+ }
+
+ [Fact]
+ public void CaseDistinctRecommendationIdsRemainDistinct()
+ {
+ var custom = new RecommendedAutoTier("BALANCE");
+ Assert.NotEqual(RecommendedAutoTier.Balance, custom);
+ Assert.Equal(2, new HashSet { RecommendedAutoTier.Balance, custom }.Count);
+ Assert.Equal("\"BALANCE\"", JsonSerializer.Serialize(custom, JsonOptions));
+ Assert.Equal(custom, JsonSerializer.Deserialize("\"BALANCE\"", JsonOptions));
+ }
+}
diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs
index d78ee57932..9ea734823e 100644
--- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs
+++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs
@@ -2503,7 +2503,8 @@ public async Task SendAndWaitAsync_Waits_For_Earlier_Idle_Handler()
using var subscription = session.On(_ =>
{
entered.TrySetResult();
- handlerFinished = release.Wait(TimeSpan.FromSeconds(5));
+ release.Wait();
+ handlerFinished = true;
});
var pending = session.SendAndWaitAsync(new MessageOptions { Prompt = "hello" });
await WaitForRequestAsync(server, "session.send");
@@ -2613,7 +2614,8 @@ public async Task CreateSessionAsync_Serializes_ManagedSettings_Permissions()
DisableBypassPermissionsMode = DisableBypassPermissionsModes.Disable,
Deny = ["shell(rm*)"],
Ask = ["write"],
- Allow = []
+ Allow = [],
+ LimitTo = ["Domain(github.com)"]
}
},
OnPermissionRequest = (_, invocation) =>
@@ -2630,6 +2632,9 @@ public async Task CreateSessionAsync_Serializes_ManagedSettings_Permissions()
Assert.Equal("shell(rm*)", Assert.Single(permissions.GetProperty("deny").EnumerateArray()).GetString());
Assert.Equal("write", Assert.Single(permissions.GetProperty("ask").EnumerateArray()).GetString());
Assert.Empty(permissions.GetProperty("allow").EnumerateArray());
+ Assert.Equal(
+ "Domain(github.com)",
+ Assert.Single(permissions.GetProperty("limitTo").EnumerateArray()).GetString());
DispatchEvent(session, new PermissionRequestedEvent
{
@@ -2999,7 +3004,7 @@ private static Process StartExitedProcess()
return process;
}
- private sealed class FakeCopilotServer : IAsyncDisposable
+ private sealed partial class FakeCopilotServer : IAsyncDisposable
{
private readonly TcpListener _listener;
private readonly CancellationTokenSource _cts = new();
@@ -3233,8 +3238,9 @@ private async Task RunAsync()
{
if (root.TryGetProperty("error", out var error))
{
- completion.TrySetException(new InvalidOperationException(
- error.GetProperty("message").GetString()));
+ var exception = new InvalidOperationException(error.GetProperty("message").GetString());
+ exception.Data["error"] = error.Clone();
+ completion.TrySetException(exception);
}
else
{
diff --git a/dotnet/test/Unit/ClientSessionSkillProviderTests.cs b/dotnet/test/Unit/ClientSessionSkillProviderTests.cs
new file mode 100644
index 0000000000..c99d1f090a
--- /dev/null
+++ b/dotnet/test/Unit/ClientSessionSkillProviderTests.cs
@@ -0,0 +1,397 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+#if NET8_0_OR_GREATER
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Xunit;
+
+namespace GitHub.Copilot.Test.Unit;
+
+public sealed partial class ClientSessionLifetimeTests
+{
+ [Fact]
+ public async Task SkillProvider_Create_And_Resume_Set_Flag_Only_When_Provided()
+ {
+ await using var server = await FakeCopilotServer.StartAsync();
+ await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
+ var provider = new TestSkillProvider
+ {
+ ReadHandler = (name, _) => Task.FromResult($"# {name}")
+ };
+
+ await using var created = await client.CreateSessionAsync(new SessionConfig
+ {
+ SessionId = "skill-create",
+ SkillProvider = provider
+ });
+ await using var resumed = await client.ResumeSessionAsync("skill-resume", new ResumeSessionConfig
+ {
+ SkillProvider = provider
+ });
+
+ Assert.True(Assert.Single(server.Requests, request => request.Method == "session.create")
+ .Params.GetProperty("hasSkillProvider").GetBoolean());
+ Assert.True(Assert.Single(server.Requests, request => request.Method == "session.resume")
+ .Params.GetProperty("hasSkillProvider").GetBoolean());
+
+ var read = await server.SendRequestAsync("skillProvider.read", SkillReadRequest(resumed.SessionId, "resumed-skill"));
+ Assert.Equal("# resumed-skill", read.GetProperty("markdown").GetString());
+
+ await created.DisposeAsync();
+ await resumed.DisposeAsync();
+ server.ClearRequests();
+
+ await using var createWithoutProvider = await client.CreateSessionAsync(new SessionConfig
+ {
+ SessionId = "skill-create-no-provider"
+ });
+ await using var resumeWithoutProvider = await client.ResumeSessionAsync("skill-resume-no-provider", new ResumeSessionConfig());
+
+ Assert.False(Assert.Single(server.Requests, request => request.Method == "session.create")
+ .Params.TryGetProperty("hasSkillProvider", out _));
+ Assert.False(Assert.Single(server.Requests, request => request.Method == "session.resume")
+ .Params.TryGetProperty("hasSkillProvider", out _));
+ }
+
+ [Fact]
+ public async Task SkillProvider_Callbacks_Work_Before_Create_And_Resume_Response()
+ {
+ await using var server = await FakeCopilotServer.StartAsync();
+ await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
+ var provider = new TestSkillProvider();
+ var callbackResults = new List>();
+ server.BeforeResponseAsync = async (request, _) =>
+ {
+ if (request.Method is "session.create" or "session.resume"
+ && request.Params.TryGetProperty("sessionId", out var sessionId))
+ {
+ callbackResults.Add(await server.SendRequestWithoutWaitingAsync(
+ "skillProvider.list",
+ SkillListRequest(sessionId.GetString()!)));
+ }
+ };
+
+ await using var created = await client.CreateSessionAsync(new SessionConfig
+ {
+ SessionId = "pre-response-create",
+ SkillProvider = provider
+ });
+ await using var resumed = await client.ResumeSessionAsync("pre-response-resume", new ResumeSessionConfig
+ {
+ SkillProvider = provider
+ });
+
+ Assert.Equal(2, callbackResults.Count);
+ foreach (var callback in callbackResults)
+ {
+ var result = await callback.WaitAsync(TimeSpan.FromSeconds(5));
+ Assert.Equal("dynamic", Assert.Single(result.GetProperty("skills").EnumerateArray()).GetProperty("name").GetString());
+ }
+ }
+
+ [Fact]
+ public async Task SkillProvider_List_Returns_CamelCase_Descriptors_And_Omits_Null_Optionals()
+ {
+ await using var server = await FakeCopilotServer.StartAsync();
+ await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
+ var provider = new TestSkillProvider
+ {
+ ListHandler = _ => Task.FromResult>(
+ [
+ new()
+ {
+ Name = "basic",
+ Description = "Basic skill"
+ },
+ new()
+ {
+ Name = "advanced",
+ Description = "Advanced skill",
+ UserInvocable = false,
+ DisableModelInvocation = true,
+ ArgumentHint = ""
+ }
+ ])
+ };
+ await using var session = await client.CreateSessionAsync(new SessionConfig { SkillProvider = provider });
+
+ var result = await server.SendRequestAsync("skillProvider.list", SkillListRequest(session.SessionId));
+ var skills = result.GetProperty("skills").EnumerateArray().ToArray();
+
+ Assert.Equal("basic", skills[0].GetProperty("name").GetString());
+ Assert.Equal("Basic skill", skills[0].GetProperty("description").GetString());
+ Assert.False(skills[0].TryGetProperty("userInvocable", out _));
+ Assert.False(skills[0].TryGetProperty("disableModelInvocation", out _));
+ Assert.False(skills[0].TryGetProperty("argumentHint", out _));
+ Assert.False(skills[0].TryGetProperty("UserInvocable", out _));
+ Assert.False(skills[0].TryGetProperty("DisableModelInvocation", out _));
+ Assert.False(skills[0].TryGetProperty("ArgumentHint", out _));
+ Assert.Equal("advanced", skills[1].GetProperty("name").GetString());
+ Assert.False(skills[1].GetProperty("userInvocable").GetBoolean());
+ Assert.True(skills[1].GetProperty("disableModelInvocation").GetBoolean());
+ Assert.Equal("", skills[1].GetProperty("argumentHint").GetString());
+ }
+
+ [Fact]
+ public async Task SkillProvider_Null_List_Result_Is_Empty()
+ {
+ await using var server = await FakeCopilotServer.StartAsync();
+ await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
+ var provider = new TestSkillProvider
+ {
+ ListHandler = _ => Task.FromResult>(null!)
+ };
+ await using var session = await client.CreateSessionAsync(new SessionConfig { SkillProvider = provider });
+
+ var result = await server.SendRequestAsync("skillProvider.list", SkillListRequest(session.SessionId));
+
+ Assert.Empty(result.GetProperty("skills").EnumerateArray());
+ }
+
+ [Fact]
+ public async Task SkillProvider_Read_Returns_Markdown_And_Null_For_Missing_Skill()
+ {
+ await using var server = await FakeCopilotServer.StartAsync();
+ await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
+ var provider = new TestSkillProvider
+ {
+ ReadHandler = (name, _) => Task.FromResult(name == "known" ? "# Known" : null)
+ };
+ await using var session = await client.CreateSessionAsync(new SessionConfig { SkillProvider = provider });
+
+ var read = await server.SendRequestAsync("skillProvider.read", SkillReadRequest(session.SessionId, "known"));
+
+ Assert.Equal("# Known", read.GetProperty("markdown").GetString());
+ var missing = await server.SendRequestAsync("skillProvider.read", SkillReadRequest(session.SessionId, "missing"));
+ Assert.True(missing.TryGetProperty("markdown", out var markdown), missing.ToString());
+ Assert.Equal(JsonValueKind.Null, markdown.ValueKind);
+ }
+
+ [Fact]
+ public async Task SkillProvider_Provider_Failures_Do_Not_Leak_Exception_Details()
+ {
+ await using var server = await FakeCopilotServer.StartAsync();
+ var logger = new SkillProviderLogger();
+ await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url), Logger = logger });
+ var listFailure = new InvalidOperationException("secret list failure");
+ var readFailure = new InvalidOperationException("secret read failure");
+ var provider = new TestSkillProvider
+ {
+ ListHandler = _ => throw listFailure,
+ ReadHandler = (_, _) => throw readFailure
+ };
+ await using var session = await client.CreateSessionAsync(new SessionConfig { SkillProvider = provider });
+
+ var listError = await Assert.ThrowsAsync(() =>
+ server.SendRequestAsync("skillProvider.list", SkillListRequest(session.SessionId)));
+ AssertRpcError(listError, "Skill provider listSkills failed");
+
+ var readError = await Assert.ThrowsAsync(() =>
+ server.SendRequestAsync("skillProvider.read", SkillReadRequest(session.SessionId, "throws")));
+ AssertRpcError(readError, "Skill provider readSkill failed");
+
+ Assert.DoesNotContain("secret", listError.Message, StringComparison.OrdinalIgnoreCase);
+ Assert.DoesNotContain("secret", readError.Message, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains(logger.Entries, entry =>
+ entry.Message == $"Skill provider listSkills failed. SessionId={session.SessionId}" && entry.Exception == listFailure);
+ Assert.Contains(logger.Entries, entry =>
+ entry.Message == $"Skill provider readSkill failed. SessionId={session.SessionId}" && entry.Exception == readFailure);
+ }
+
+ [Fact]
+ public async Task SkillProvider_Provider_Cancellation_Without_Request_Cancellation_Is_Provider_Failure()
+ {
+ await using var server = await FakeCopilotServer.StartAsync();
+ await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
+ var provider = new TestSkillProvider
+ {
+ ListHandler = _ => throw new TaskCanceledException("secret list timeout"),
+ ReadHandler = (_, _) => throw new TaskCanceledException("secret read timeout")
+ };
+ await using var session = await client.CreateSessionAsync(new SessionConfig { SkillProvider = provider });
+
+ var listError = await Assert.ThrowsAsync(() =>
+ server.SendRequestAsync("skillProvider.list", SkillListRequest(session.SessionId)).WaitAsync(TimeSpan.FromSeconds(10)));
+ AssertRpcError(listError, "Skill provider listSkills failed");
+
+ var readError = await Assert.ThrowsAsync(() =>
+ server.SendRequestAsync("skillProvider.read", SkillReadRequest(session.SessionId, "times-out")).WaitAsync(TimeSpan.FromSeconds(10)));
+ AssertRpcError(readError, "Skill provider readSkill failed");
+ }
+
+ [Fact]
+ public async Task SkillProvider_Rejects_Unknown_NoProvider_And_Disposed_Sessions()
+ {
+ await using var server = await FakeCopilotServer.StartAsync();
+ await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
+ await client.StartAsync();
+
+ var unknown = await Assert.ThrowsAsync(() =>
+ server.SendRequestAsync("skillProvider.list", SkillListRequest("unknown-session")));
+ AssertRpcError(unknown, "No skill provider for session: unknown-session");
+
+ await using var noProvider = await client.CreateSessionAsync(new SessionConfig { SessionId = "no-provider" });
+ var noProviderError = await Assert.ThrowsAsync(() =>
+ server.SendRequestAsync("skillProvider.list", SkillListRequest(noProvider.SessionId)));
+ AssertRpcError(noProviderError, "No skill provider for session: no-provider");
+
+ var providerSession = await client.CreateSessionAsync(new SessionConfig
+ {
+ SessionId = "disposed-provider",
+ SkillProvider = new TestSkillProvider()
+ });
+ await providerSession.DisposeAsync();
+ var disposed = await Assert.ThrowsAsync(() =>
+ server.SendRequestAsync("skillProvider.list", SkillListRequest("disposed-provider")));
+ AssertRpcError(disposed, "No skill provider for session: disposed-provider");
+ }
+
+ [Fact]
+ public async Task SkillProvider_Cloud_Create_Throws_Before_Connecting()
+ {
+ await using var client = new CopilotClient(new CopilotClientOptions
+ {
+ Connection = RuntimeConnection.ForUri("http://127.0.0.1:1")
+ });
+ var provider = new TestSkillProvider();
+
+ var error = await Assert.ThrowsAsync(() => client.CreateSessionAsync(new SessionConfig
+ {
+ Cloud = new CloudSessionOptions
+ {
+ Repository = new CloudSessionRepository
+ {
+ Owner = "github",
+ Name = "copilot-sdk",
+ Branch = "main"
+ }
+ },
+ SkillProvider = provider
+ }));
+
+ Assert.Equal("Skill providers are not supported for cloud sessions.", error.Message);
+ Assert.Equal(0, provider.ListCalls);
+ Assert.Equal(0, provider.ReadCalls);
+ }
+
+ [Fact]
+ public async Task SkillProvider_EmptyMode_Defaults_EnableSkills_To_False()
+ {
+ await using var server = await FakeCopilotServer.StartAsync();
+ await using var client = new CopilotClient(new CopilotClientOptions
+ {
+ Connection = RuntimeConnection.ForUri(server.Url),
+ Mode = CopilotClientMode.Empty,
+ BaseDirectory = Path.GetTempPath(),
+ });
+
+ await using var session = await client.CreateSessionAsync(new SessionConfig
+ {
+ AvailableTools = [],
+ SkillProvider = new TestSkillProvider()
+ });
+
+ var request = Assert.Single(server.Requests, request => request.Method == "session.create");
+ Assert.True(request.Params.GetProperty("hasSkillProvider").GetBoolean());
+ Assert.False(request.Params.GetProperty("enableSkills").GetBoolean());
+ }
+
+ private static Dictionary SkillListRequest(string sessionId) => new()
+ {
+ ["sessionId"] = sessionId
+ };
+
+ private static Dictionary SkillReadRequest(string sessionId, string name) => new()
+ {
+ ["sessionId"] = sessionId,
+ ["name"] = name
+ };
+
+ private static void AssertRpcError(InvalidOperationException exception, string expectedMessage)
+ {
+ Assert.Equal(expectedMessage, exception.Message);
+ var error = Assert.IsType(exception.Data["error"]);
+ Assert.Equal(-32603, error.GetProperty("code").GetInt32());
+ Assert.Equal(expectedMessage, error.GetProperty("message").GetString());
+ Assert.False(error.TryGetProperty("data", out _), error.ToString());
+ }
+
+ private sealed class SkillProviderLogger : ILogger
+ {
+ private readonly ConcurrentQueue<(string Message, Exception? Exception)> _entries = new();
+
+ public IReadOnlyCollection<(string Message, Exception? Exception)> Entries => _entries;
+
+ public IDisposable? BeginScope(TState state) where TState : notnull => null;
+
+ public bool IsEnabled(LogLevel logLevel) => true;
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) =>
+ _entries.Enqueue((formatter(state, exception), exception));
+ }
+
+ private sealed class TestSkillProvider : ISkillProvider
+ {
+ public int ListCalls;
+ public int ReadCalls;
+
+ public Func>> ListHandler { get; init; } =
+ _ => Task.FromResult>(
+ [
+ new()
+ {
+ Name = "dynamic",
+ Description = "Dynamic skill"
+ }
+ ]);
+
+ public Func> ReadHandler { get; init; } =
+ (name, _) => Task.FromResult($"# {name}");
+
+ public Task> ListSkillsAsync(CancellationToken cancellationToken)
+ {
+ Interlocked.Increment(ref ListCalls);
+ return ListHandler(cancellationToken);
+ }
+
+ public Task ReadSkillAsync(string name, CancellationToken cancellationToken)
+ {
+ Interlocked.Increment(ref ReadCalls);
+ return ReadHandler(name, cancellationToken);
+ }
+ }
+
+ private sealed partial class FakeCopilotServer
+ {
+ public async Task> SendRequestWithoutWaitingAsync(string method, Dictionary parameters)
+ {
+ var stream = _stream ?? throw new InvalidOperationException("Client is not connected.");
+ var id = Interlocked.Increment(ref _nextRequestId);
+ var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ if (!_pendingRequests.TryAdd(id, completion))
+ {
+ throw new InvalidOperationException("Failed to track callback request.");
+ }
+
+ await WriteMessageAsync(stream, new Dictionary
+ {
+ ["jsonrpc"] = "2.0",
+ ["id"] = id,
+ ["method"] = method,
+ ["params"] = parameters
+ }, _cts.Token);
+
+ return completion.Task.WaitAsync(_cts.Token);
+ }
+ }
+}
+#endif
diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs
index c67bce8b4a..3ae4c0b009 100644
--- a/dotnet/test/Unit/CloneTests.cs
+++ b/dotnet/test/Unit/CloneTests.cs
@@ -88,6 +88,15 @@ private sealed class TestExtensionLaunchProvider : GitHub.Copilot.Rpc.IExtension
Task.FromResult(new GitHub.Copilot.Rpc.ExtensionLaunchProviderResolveResult());
}
+ private sealed class TestSkillProvider : ISkillProvider
+ {
+ public Task> ListSkillsAsync(CancellationToken cancellationToken) =>
+ Task.FromResult>([]);
+
+ public Task ReadSkillAsync(string name, CancellationToken cancellationToken) =>
+ Task.FromResult(null);
+ }
+
[Fact]
public void SessionConfig_Clone_CopiesAllProperties()
{
@@ -205,6 +214,28 @@ public void SessionConfig_RefreshCustomInstructions_IsCreateOnlyAndDefaultsToNul
Assert.Null(typeof(ResumeSessionConfig).GetProperty(nameof(SessionConfig.RefreshCustomInstructions)));
}
+ [Fact]
+ public void SessionConfig_Clone_CopiesSkillProvider()
+ {
+ var provider = new TestSkillProvider();
+ var original = new SessionConfig { SkillProvider = provider };
+
+ var clone = original.Clone();
+
+ Assert.Same(provider, clone.SkillProvider);
+ }
+
+ [Fact]
+ public void ResumeSessionConfig_Clone_CopiesSkillProvider()
+ {
+ var provider = new TestSkillProvider();
+ var original = new ResumeSessionConfig { SkillProvider = provider };
+
+ var clone = original.Clone();
+
+ Assert.Same(provider, clone.SkillProvider);
+ }
+
[Fact]
public void SessionConfig_Clone_CollectionsAreIndependent()
{
diff --git a/dotnet/test/Unit/RpcMcpListTests.cs b/dotnet/test/Unit/RpcMcpListTests.cs
new file mode 100644
index 0000000000..6143a6fbbb
--- /dev/null
+++ b/dotnet/test/Unit/RpcMcpListTests.cs
@@ -0,0 +1,43 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+#if NET8_0_OR_GREATER
+using GitHub.Copilot.Rpc;
+using Xunit;
+
+namespace GitHub.Copilot.Test.Unit;
+
+public sealed partial class ClientSessionLifetimeTests
+{
+ [Fact]
+ public async Task Session_Rpc_Mcp_List_And_ListConfigured_Use_Parameterless_Wire_Contracts()
+ {
+ await using var server = await FakeCopilotServer.StartAsync();
+ server.ResponseFactory = _ => new Dictionary { ["servers"] = Array.Empty