diff --git a/.github/actions/java-test-report/action.yml b/.github/actions/java-test-report/action.yml
index eedf053725..4ea110cb13 100644
--- a/.github/actions/java-test-report/action.yml
+++ b/.github/actions/java-test-report/action.yml
@@ -126,7 +126,8 @@ runs:
if [ -n "$covered" ] && [ -n "$missed" ]; then
local total=$((covered + missed))
if [ "$total" -gt 0 ]; then
- echo "scale=1; $covered * 100 / $total" | bc
+ awk -v covered="$covered" -v total="$total" \
+ 'BEGIN { printf "%.1f\n", covered * 100 / total }'
else
echo "0"
fi
diff --git a/.github/workflows/java-publish-maven.yml b/.github/workflows/java-publish-maven.yml
index 1744a697e1..1ce4f51e38 100644
--- a/.github/workflows/java-publish-maven.yml
+++ b/.github/workflows/java-publish-maven.yml
@@ -1,9 +1,6 @@
name: "Java Publish to Maven Central"
env:
- # Disable Husky Git hooks in CI to prevent local development hooks
- # (e.g., pre-commit formatting checks) from running during automated
- # workflows that perform git commits and pushes.
HUSKY: 0
on:
@@ -40,7 +37,7 @@ on:
outputs:
mavenPublished:
description: "Whether the Java package was published to Maven Central"
- value: ${{ jobs.publish-maven.outputs.published }}
+ value: ${{ jobs.deploy-maven.outputs.published }}
secrets:
JAVA_RELEASE_TOKEN:
required: true
@@ -56,8 +53,7 @@ on:
required: true
permissions:
- contents: write
- id-token: write
+ contents: read
concurrency:
group: publish-maven
@@ -70,11 +66,6 @@ jobs:
steps:
- name: Verify JAVA_RELEASE_TOKEN can push to repository
run: |
- # JAVA_RELEASE_TOKEN is used by actions/checkout and for:
- # - git push origin main (doc updates)
- # - mvn release:prepare -DpushChanges=true (release commits + tags)
- # - git revert + push (rollback on failure)
- # It must have push (contents:write) permission on this repo.
PUSH=$(gh api repos/${{ github.repository }} --jq '.permissions.push // false')
if [ "$PUSH" != "true" ]; then
echo "::error::JAVA_RELEASE_TOKEN lacks push permission on ${{ github.repository }}. It is required for pushing release commits and tags to main."
@@ -84,17 +75,24 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.JAVA_RELEASE_TOKEN }}
- publish-maven:
- name: Publish Java SDK to Maven Central
+ prepare-release:
+ name: Prepare Java release
needs: preflight
runs-on: ubuntu-latest
+ permissions:
+ contents: write
defaults:
run:
shell: bash
working-directory: ./java
outputs:
- version: ${{ steps.versions.outputs.release_version }}
- published: ${{ steps.publish-maven.outcome == 'success' }}
+ release_version: ${{ steps.versions.outputs.release_version }}
+ development_version: ${{ steps.versions.outputs.development_version }}
+ release_tag: ${{ steps.release-identity.outputs.release_tag }}
+ tag_commit: ${{ steps.release-identity.outputs.tag_commit }}
+ pre_prepare_commit: ${{ steps.pre-prepare.outputs.pre_prepare_commit }}
+ post_prepare_commit: ${{ steps.release-identity.outputs.post_prepare_commit }}
+ docs_commit: ${{ steps.update-docs.outputs.docs_commit_sha }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -108,88 +106,66 @@ jobs:
- uses: ./.github/actions/setup-copilot
- - name: Set up JDK 25
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
+ - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: "25"
distribution: "microsoft"
cache: "maven"
- server-id: central
- server-username: MAVEN_USERNAME
- server-password: MAVEN_PASSWORD
- gpg-private-key: ${{ secrets.JAVA_GPG_SECRET_KEY }}
- gpg-passphrase: JAVA_GPG_PASSPHRASE
- name: Determine versions
id: versions
- working-directory: ./java
run: |
CURRENT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
echo "Current pom.xml version: $CURRENT_VERSION"
- # Determine release version
if [ -n "${{ inputs.releaseVersion }}" ]; then
RELEASE_VERSION="${{ inputs.releaseVersion }}"
else
- # Remove -SNAPSHOT suffix if present
RELEASE_VERSION="${CURRENT_VERSION%-SNAPSHOT}"
fi
echo "Release version: $RELEASE_VERSION"
- # Determine next development version
if [ -n "${{ inputs.developmentVersion }}" ]; then
DEV_VERSION="${{ inputs.developmentVersion }}"
if [[ "$DEV_VERSION" != *-SNAPSHOT ]]; then
- echo "::error::developmentVersion '${DEV_VERSION}' must end with '-SNAPSHOT' (e.g., '${DEV_VERSION}-SNAPSHOT'). The maven-release-plugin requires the next development version to be a snapshot."
+ echo "::error::developmentVersion '${DEV_VERSION}' must end with '-SNAPSHOT'."
exit 1
fi
else
- # Split version: supports "0.1.32", "0.1.32-preview.0", "0.1.32-java.0", and "0.1.32-java-preview.0" formats
- # Validate RELEASE_VERSION format explicitly to provide clear errors
if ! echo "$RELEASE_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-(preview|(beta-)?java(-preview)?)\.[0-9]+)?$'; then
- echo "Error: RELEASE_VERSION '$RELEASE_VERSION' is invalid. Expected format: M.M.P, M.M.P-preview.N, M.M.P-java.N, M.M.P-java-preview.N, M.M.P-beta-java.N, or M.M.P-beta-java-preview.N (e.g., 1.2.3, 1.2.3-preview.0, 1.2.3-java.0, 1.2.3-java-preview.0, 1.2.3-beta-java.0, or 1.2.3-beta-java-preview.0)." >&2
+ echo "Error: RELEASE_VERSION '$RELEASE_VERSION' is invalid." >&2
exit 1
fi
- # Extract the base M.M.P portion (before any qualifier)
BASE_VERSION=$(echo "$RELEASE_VERSION" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+')
QUALIFIER=$(echo "$RELEASE_VERSION" | sed "s|^${BASE_VERSION}||")
IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION"
- NEXT_PATCH=$((PATCH + 1))
- DEV_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}${QUALIFIER}-SNAPSHOT"
+ DEV_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))${QUALIFIER}-SNAPSHOT"
fi
- echo "Next development version: $DEV_VERSION"
- echo "release_version=$RELEASE_VERSION" >> $GITHUB_OUTPUT
- echo "dev_version=$DEV_VERSION" >> $GITHUB_OUTPUT
-
- echo "### Version Summary" >> $GITHUB_STEP_SUMMARY
- echo "- **Release version:** $RELEASE_VERSION" >> $GITHUB_STEP_SUMMARY
- echo "- **Next development version:** $DEV_VERSION" >> $GITHUB_STEP_SUMMARY
+ echo "release_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT"
+ echo "development_version=$DEV_VERSION" >> "$GITHUB_OUTPUT"
- name: Update documentation with release version
id: update-docs
- working-directory: ./java
run: |
VERSION="${{ steps.versions.outputs.release_version }}"
- DEV_VERSION="${{ steps.versions.outputs.dev_version }}"
+ DEV_VERSION="${{ steps.versions.outputs.development_version }}"
./scripts/test-update-documentation-versions.sh
./scripts/update-documentation-versions.sh "$VERSION" "$DEV_VERSION" README.md sdk/jbang-example.java
-
- # Commit the documentation changes before release:prepare (requires clean working directory)
git add README.md sdk/jbang-example.java
git commit -m "docs: update version references to ${VERSION}"
-
- # Save the commit SHA for potential rollback
- echo "docs_commit_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
-
+ echo "docs_commit_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
git push origin main
+ - name: Record rollback base
+ id: pre-prepare
+ run: echo "pre_prepare_commit=$(git rev-parse HEAD^)" >> "$GITHUB_OUTPUT"
+
- name: Prepare Release
- working-directory: ./java
run: |
mvn -B release:prepare \
-DreleaseVersion=${{ steps.versions.outputs.release_version }} \
- -DdevelopmentVersion=${{ steps.versions.outputs.dev_version }} \
+ -DdevelopmentVersion=${{ steps.versions.outputs.development_version }} \
-DtagNameFormat=java/v@{project.version} \
-DpushChanges=true \
-Darguments="-DskipTests"
@@ -198,38 +174,413 @@ jobs:
MAVEN_PASSWORD: ${{ secrets.JAVA_MAVEN_CENTRAL_PASSWORD }}
JAVA_GPG_PASSPHRASE: ${{ secrets.JAVA_GPG_PASSPHRASE }}
- - name: Perform Release and Deploy to Maven Central
- id: publish-maven
+ - name: Record immutable release identity
+ id: release-identity
+ run: |
+ TAG="java/v${{ steps.versions.outputs.release_version }}"
+ TAG_COMMIT=$(git rev-parse "${TAG}^{commit}")
+ POST_PREPARE_COMMIT=$(git rev-parse HEAD)
+ echo "release_tag=$TAG" >> "$GITHUB_OUTPUT"
+ echo "tag_commit=$TAG_COMMIT" >> "$GITHUB_OUTPUT"
+ echo "post_prepare_commit=$POST_PREPARE_COMMIT" >> "$GITHUB_OUTPUT"
+
+ build-windows-classifier:
+ name: Build Windows native classifier
+ needs: prepare-release
+ runs-on: windows-latest
+ permissions:
+ contents: read
+ defaults:
+ run:
+ shell: pwsh
+ working-directory: ./java
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: ${{ needs.prepare-release.outputs.release_tag }}
+ fetch-depth: 1
+ persist-credentials: false
+
+ - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
+ with:
+ java-version: "25"
+ distribution: "microsoft"
+ cache: "maven"
+
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ with:
+ node-version: 22
+
+ - name: Build and validate win32-x64 classifier
+ run: |
+ $sourceCommit = git rev-parse HEAD
+ if ($sourceCommit -ne '${{ needs.prepare-release.outputs.tag_commit }}') {
+ throw "Checked out $sourceCommit instead of the prepared tag commit."
+ }
+ node copilot-native/scripts/validate-native-host.mjs win32-x64
+ mvn -B -pl copilot-native package -DskipTests
+ $version = '${{ needs.prepare-release.outputs.release_version }}'
+ $jar = "copilot-native/target/copilot-sdk-java-runtime-$version-win32-x64.jar"
+ $primaryJar = "copilot-native/target/copilot-sdk-java-runtime-$version.jar"
+ if (-not (Test-Path -LiteralPath $jar -PathType Leaf)) {
+ throw "Expected Windows classifier was not produced: $jar"
+ }
+ node copilot-native/scripts/validate-native-artifact.mjs classifier win32-x64 $jar ([IO.Path]::GetFileName($jar)) ..
+ node copilot-native/scripts/validate-native-artifact.mjs placeholder $primaryJar
+ $manifest = "copilot-native/target/win32-x64-$version.sha256"
+ $hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $jar).Hash.ToLowerInvariant()
+ "$hash $([IO.Path]::GetFileName($jar))" | Set-Content -NoNewline -Encoding ascii $manifest
+ node copilot-native/scripts/validate-native-artifact.mjs checksum $jar $manifest ([IO.Path]::GetFileName($jar))
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: java-native-win32-x64-release-${{ github.run_id }}-${{ github.run_attempt }}
+ path: |
+ java/copilot-native/target/copilot-sdk-java-runtime-${{ needs.prepare-release.outputs.release_version }}-win32-x64.jar
+ java/copilot-native/target/win32-x64-${{ needs.prepare-release.outputs.release_version }}.sha256
+ if-no-files-found: error
+ retention-days: 1
+
+ build-darwin-classifier:
+ name: Build Darwin native classifier
+ needs: prepare-release
+ runs-on: macos-26
+ permissions:
+ contents: read
+ defaults:
+ run:
+ shell: bash
working-directory: ./java
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: ${{ needs.prepare-release.outputs.release_tag }}
+ fetch-depth: 1
+ persist-credentials: false
+
+ - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
+ with:
+ java-version: "25"
+ distribution: "microsoft"
+ cache: "maven"
+
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ with:
+ node-version: 22
+
+ - name: Build and validate darwin-arm64 classifier
run: |
- mvn -B release:perform \
- -Dgoals="deploy" \
- -Darguments="-DskipTests -Prelease"
+ set -euo pipefail
+ SOURCE_COMMIT=$(git rev-parse HEAD)
+ if [ "$SOURCE_COMMIT" != "${{ needs.prepare-release.outputs.tag_commit }}" ]; then
+ echo "::error::Checked out $SOURCE_COMMIT instead of the prepared tag commit."
+ exit 1
+ fi
+ node copilot-native/scripts/validate-native-host.mjs darwin-arm64
+ mvn -B -pl copilot-native package -DskipTests
+ VERSION="${{ needs.prepare-release.outputs.release_version }}"
+ JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-darwin-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 darwin-arm64 "$JAR" "$(basename "$JAR")" ..
+ node copilot-native/scripts/validate-native-artifact.mjs placeholder "$PRIMARY_JAR"
+ MANIFEST="copilot-native/target/darwin-arm64-$VERSION.sha256"
+ HASH=$(shasum -a 256 "$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")"
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: java-native-darwin-arm64-release-${{ github.run_id }}-${{ github.run_attempt }}
+ path: |
+ java/copilot-native/target/copilot-sdk-java-runtime-${{ needs.prepare-release.outputs.release_version }}-darwin-arm64.jar
+ java/copilot-native/target/darwin-arm64-${{ needs.prepare-release.outputs.release_version }}.sha256
+ if-no-files-found: error
+ retention-days: 1
+
+ deploy-maven:
+ name: Deploy Java release to Maven Central
+ needs: [prepare-release, build-windows-classifier, build-darwin-classifier]
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ defaults:
+ run:
+ shell: bash
+ working-directory: ./java
+ outputs:
+ version: ${{ needs.prepare-release.outputs.release_version }}
+ published: ${{ steps.publish-maven.outcome == 'success' }}
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: ${{ needs.prepare-release.outputs.release_tag }}
+ fetch-depth: 1
+ persist-credentials: false
+
+ - uses: ./.github/actions/setup-copilot
+
+ - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
+ with:
+ java-version: "25"
+ distribution: "microsoft"
+ cache: "maven"
+ server-id: central
+ server-username: MAVEN_USERNAME
+ server-password: MAVEN_PASSWORD
+ gpg-private-key: ${{ secrets.JAVA_GPG_SECRET_KEY }}
+ gpg-passphrase: JAVA_GPG_PASSPHRASE
+
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ with:
+ node-version: 22
+
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: java-native-win32-x64-release-${{ github.run_id }}-${{ github.run_attempt }}
+ path: ${{ runner.temp }}/java-native-win32-x64
+
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: java-native-darwin-arm64-release-${{ github.run_id }}-${{ github.run_attempt }}
+ path: ${{ runner.temp }}/java-native-darwin-arm64
+
+ - name: Verify immutable source and Windows classifier
+ id: windows-artifact
+ run: |
+ SOURCE_COMMIT=$(git rev-parse HEAD)
+ if [ "$SOURCE_COMMIT" != "${{ needs.prepare-release.outputs.tag_commit }}" ]; then
+ echo "::error::Checked out $SOURCE_COMMIT instead of the prepared tag commit."
+ exit 1
+ fi
+ VERSION="${{ needs.prepare-release.outputs.release_version }}"
+ ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-win32-x64"
+ JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-win32-x64.jar"
+ MANIFEST="$ARTIFACT_DIRECTORY/win32-x64-$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 win32-x64 "$JAR" "$(basename "$JAR")" "$GITHUB_WORKSPACE"
+ echo "windows_jar=$JAR" >> "$GITHUB_OUTPUT"
+ echo "windows_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT"
+
+ - name: Verify immutable source and Darwin classifier
+ id: darwin-artifact
+ run: |
+ SOURCE_COMMIT=$(git rev-parse HEAD)
+ if [ "$SOURCE_COMMIT" != "${{ needs.prepare-release.outputs.tag_commit }}" ]; then
+ echo "::error::Checked out $SOURCE_COMMIT instead of the prepared tag commit."
+ exit 1
+ fi
+ VERSION="${{ needs.prepare-release.outputs.release_version }}"
+ ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-darwin-arm64"
+ JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-darwin-arm64.jar"
+ MANIFEST="$ARTIFACT_DIRECTORY/darwin-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 darwin-arm64 "$JAR" "$(basename "$JAR")" "$GITHUB_WORKSPACE"
+ echo "darwin_jar=$JAR" >> "$GITHUB_OUTPUT"
+ echo "darwin_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT"
+
+ - name: Build Linux classifier and deploy complete release
+ id: publish-maven
+ run: |
+ VERSION="${{ needs.prepare-release.outputs.release_version }}"
+ mvn -B deploy -DskipTests -Prelease -Dcopilot.native.libc=glibc \
+ "-Dcopilot.native.external.win32.classifier.path=${{ steps.windows-artifact.outputs.windows_jar }}" \
+ "-Dcopilot.native.external.darwin.classifier.path=${{ steps.darwin-artifact.outputs.darwin_jar }}"
+ LINUX_JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-linux-x64.jar"
+ test -f "$LINUX_JAR"
+ node copilot-native/scripts/validate-native-artifact.mjs \
+ classifier linux-x64 "$LINUX_JAR" "$(basename "$LINUX_JAR")" ..
+ LINUX_SHA=$(sha256sum "$LINUX_JAR" | cut -d ' ' -f 1)
+ GROUP_ID=$(mvn -q -pl copilot-native help:evaluate -Dexpression=project.groupId -DforceStdout)
+ ARTIFACT_ID=$(mvn -q -pl copilot-native help:evaluate -Dexpression=project.artifactId -DforceStdout)
+ POM_VERSION=$(mvn -q -pl copilot-native help:evaluate -Dexpression=project.version -DforceStdout)
+ if [ -z "$GROUP_ID" ] || [ -z "$ARTIFACT_ID" ] || [ "$POM_VERSION" != "$VERSION" ]; then
+ echo "::error::Unexpected copilot-native Maven coordinates: $GROUP_ID:$ARTIFACT_ID:$POM_VERSION (expected version $VERSION)"
+ exit 1
+ fi
+ {
+ echo "### Maven Central Release"
+ echo "- **Version:** $VERSION"
+ echo "- **Source tag:** \`${{ needs.prepare-release.outputs.release_tag }}\`"
+ echo "- **Source commit:** \`${{ needs.prepare-release.outputs.tag_commit }}\`"
+ echo "- **Next development version:** ${{ needs.prepare-release.outputs.development_version }}"
+ echo "- **Repository:** Maven Central"
+ echo ""
+ echo "#### Maven Coordinates"
+ echo ""
+ echo '```xml'
+ echo ""
+ echo " $GROUP_ID"
+ echo " $ARTIFACT_ID"
+ echo " $POM_VERSION"
+ echo ""
+ echo '```'
+ echo ""
+ echo "#### Published Native Classifiers"
+ echo ""
+ echo "| Classifier | Build runner | Artifact | SHA-256 | Status |"
+ echo "| --- | --- | --- | --- | --- |"
+ echo "| \`linux-x64\` | \`ubuntu-latest\` | \`$(basename "$LINUX_JAR")\` | \`$LINUX_SHA\` | Published |"
+ echo "| \`win32-x64\` | \`windows-latest\` | \`$(basename "${{ steps.windows-artifact.outputs.windows_jar }}")\` | \`${{ steps.windows-artifact.outputs.windows_sha }}\` | Published |"
+ echo "| \`darwin-arm64\` | \`macos-26\` | \`$(basename "${{ steps.darwin-artifact.outputs.darwin_jar }}")\` | \`${{ steps.darwin-artifact.outputs.darwin_sha }}\` | Published |"
+ } >> "$GITHUB_STEP_SUMMARY"
env:
MAVEN_USERNAME: ${{ secrets.JAVA_MAVEN_CENTRAL_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.JAVA_MAVEN_CENTRAL_PASSWORD }}
JAVA_GPG_PASSPHRASE: ${{ secrets.JAVA_GPG_PASSPHRASE }}
- - name: Rollback documentation commit on failure
- if: failure() && steps.update-docs.outputs.docs_commit_sha != ''
+ rollback-release:
+ name: Roll back failed Java release preparation
+ needs:
+ [
+ prepare-release,
+ build-windows-classifier,
+ build-darwin-classifier,
+ deploy-maven,
+ ]
+ if: ${{ failure() && needs.prepare-release.outputs.docs_commit != '' }}
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ defaults:
+ run:
+ shell: bash
working-directory: ./java
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+ token: ${{ secrets.JAVA_RELEASE_TOKEN }}
+
+ - name: Safely revert release commits and tag
+ env:
+ DOCS_COMMIT: ${{ needs.prepare-release.outputs.docs_commit }}
+ PREPARE_RESULT: ${{ needs.prepare-release.result }}
+ PRE_PREPARE_COMMIT: ${{ needs.prepare-release.outputs.pre_prepare_commit }}
+ POST_PREPARE_COMMIT: ${{ needs.prepare-release.outputs.post_prepare_commit }}
+ RELEASE_TAG: java/v${{ needs.prepare-release.outputs.release_version }}
+ TAG_COMMIT: ${{ needs.prepare-release.outputs.tag_commit }}
run: |
- echo "Release failed, rolling back documentation commit..."
- git revert --no-edit ${{ steps.update-docs.outputs.docs_commit_sha }}
- git push origin main
+ set -euo pipefail
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git fetch origin main --tags
+ MAIN_COMMIT=$(git rev-parse origin/main)
+ echo "Rollback inspection: main=$MAIN_COMMIT docs=$DOCS_COMMIT pre=$PRE_PREPARE_COMMIT post=$POST_PREPARE_COMMIT tag=$RELEASE_TAG"
+
+ unsafe_rollback() {
+ echo "::error::Unsafe rollback state: $*" >&2
+ exit 1
+ }
+
+ if ! git cat-file -e "${PRE_PREPARE_COMMIT}^{commit}"; then
+ unsafe_rollback "Recorded pre-prepare commit does not exist: $PRE_PREPARE_COMMIT"
+ fi
+ if ! git cat-file -e "${DOCS_COMMIT}^{commit}"; then
+ unsafe_rollback "Recorded documentation commit does not exist: $DOCS_COMMIT"
+ fi
+ if ! git merge-base --is-ancestor "$PRE_PREPARE_COMMIT" "$MAIN_COMMIT"; then
+ unsafe_rollback "Recorded pre-prepare commit is not an ancestor of main."
+ fi
+ if [ "$(git rev-parse "${DOCS_COMMIT}^")" != "$PRE_PREPARE_COMMIT" ]; then
+ unsafe_rollback "Documentation commit is not immediately based on the recorded pre-prepare commit."
+ fi
+
+ mapfile -t ROLLBACK_COMMITS < <(git rev-list --reverse "$PRE_PREPARE_COMMIT..$MAIN_COMMIT")
+ FIRST_PARENT_COUNT=$(git rev-list --count --first-parent "$PRE_PREPARE_COMMIT..$MAIN_COMMIT")
+ if [ "${#ROLLBACK_COMMITS[@]}" -ne "$FIRST_PARENT_COUNT" ]; then
+ unsafe_rollback "Release range contains merged history."
+ fi
+ if [ "${#ROLLBACK_COMMITS[@]}" -lt 1 ] || [ "${#ROLLBACK_COMMITS[@]}" -gt 3 ]; then
+ unsafe_rollback "Expected one to three release-preparation commits after the recorded base; found ${#ROLLBACK_COMMITS[@]}."
+ fi
+ if [ "${ROLLBACK_COMMITS[0]}" != "$DOCS_COMMIT" ]; then
+ unsafe_rollback "The first commit after the recorded base is not the recorded documentation commit."
+ fi
+
+ EXPECTED_PARENT="$PRE_PREPARE_COMMIT"
+ for COMMIT in "${ROLLBACK_COMMITS[@]}"; do
+ if git rev-parse -q --verify "${COMMIT}^2" >/dev/null; then
+ unsafe_rollback "Release range contains merge commit $COMMIT."
+ fi
+ if [ "$(git rev-parse "${COMMIT}^")" != "$EXPECTED_PARENT" ]; then
+ unsafe_rollback "Release range is not a linear continuation of the recorded base."
+ fi
+ EXPECTED_PARENT="$COMMIT"
+ done
+
+ RELEASE_VERSION="${RELEASE_TAG#java/v}"
+ if [ "$(git log -1 --format=%s "$DOCS_COMMIT")" != "docs: update version references to $RELEASE_VERSION" ]; then
+ unsafe_rollback "Recorded documentation commit has an unexpected subject."
+ fi
- # Also run Maven release:rollback to clean up any partial release state
- mvn -B release:rollback || true
+ RELEASE_PREPARE_COMMIT=""
+ if [ "${#ROLLBACK_COMMITS[@]}" -ge 2 ]; then
+ RELEASE_PREPARE_COMMIT="${ROLLBACK_COMMITS[1]}"
+ if [ "$(git log -1 --format=%s "$RELEASE_PREPARE_COMMIT")" != "[maven-release-plugin] prepare release $RELEASE_TAG" ]; then
+ unsafe_rollback "Release-version commit has an unexpected subject."
+ fi
+ fi
+ if [ "${#ROLLBACK_COMMITS[@]}" -eq 3 ] && [ "$(git log -1 --format=%s "${ROLLBACK_COMMITS[2]}")" != "[maven-release-plugin] prepare for next development iteration" ]; then
+ unsafe_rollback "Development-version commit has an unexpected subject."
+ fi
+
+ TAG_OBJECT=$(git ls-remote --refs origin "refs/tags/$RELEASE_TAG" | awk '{print $1}')
+ if [ -n "$TAG_OBJECT" ]; then
+ git fetch --no-tags origin "+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"
+ REMOTE_TAG_COMMIT=$(git rev-parse "${RELEASE_TAG}^{commit}")
+ if [ -z "$RELEASE_PREPARE_COMMIT" ] || [ "$REMOTE_TAG_COMMIT" != "$RELEASE_PREPARE_COMMIT" ]; then
+ unsafe_rollback "Release tag does not point to the guarded release-version commit."
+ fi
+ elif [ -n "$TAG_COMMIT" ]; then
+ unsafe_rollback "Recorded release tag is absent from the remote."
+ fi
+
+ if [ "$PREPARE_RESULT" = "success" ]; then
+ if [ -z "$POST_PREPARE_COMMIT" ] || [ -z "$TAG_COMMIT" ]; then
+ unsafe_rollback "Successful preparation did not record its immutable release identity."
+ fi
+ if [ "$MAIN_COMMIT" != "$POST_PREPARE_COMMIT" ]; then
+ unsafe_rollback "main has advanced beyond the recorded post-prepare commit."
+ fi
+ if [ -z "$TAG_OBJECT" ] || [ "$REMOTE_TAG_COMMIT" != "$TAG_COMMIT" ]; then
+ unsafe_rollback "Remote release tag does not match the recorded tag commit."
+ fi
+ fi
+
+ git checkout -B release-rollback "$MAIN_COMMIT"
+ git revert --no-edit "$PRE_PREPARE_COMMIT..$MAIN_COMMIT"
+ if [ -n "$TAG_OBJECT" ]; then
+ git push --atomic \
+ "--force-with-lease=refs/heads/main:$MAIN_COMMIT" \
+ "--force-with-lease=refs/tags/$RELEASE_TAG:$TAG_OBJECT" \
+ origin HEAD:refs/heads/main ":refs/tags/$RELEASE_TAG"
+ else
+ git push \
+ "--force-with-lease=refs/heads/main:$MAIN_COMMIT" \
+ origin HEAD:refs/heads/main
+ fi
+ echo "Release preparation rollback completed after guarded history inspection."
deploy-site:
name: Deploy Documentation Site
- needs: [preflight, publish-maven]
- if: github.ref == 'refs/heads/main'
+ needs: [preflight, deploy-maven]
+ if: github.ref == 'refs/heads/main' && needs.deploy-maven.outputs.published == 'true'
runs-on: ubuntu-latest
steps:
- name: Trigger site deployment on standalone repo
run: |
- VERSION="${{ needs.publish-maven.outputs.version }}"
+ VERSION="${{ needs.deploy-maven.outputs.version }}"
TAG="java/v${VERSION}"
PUBLISH_AS_LATEST=true
if [ "${{ inputs.prerelease }}" = "true" ]; then
@@ -241,7 +592,9 @@ jobs:
-f version="${VERSION}" \
-f publish_as_latest="${PUBLISH_AS_LATEST}" \
-f monorepo_tag="${TAG}"
- echo "### Site Deployment" >> $GITHUB_STEP_SUMMARY
- echo "Triggered deploy-site.yml on github/copilot-sdk-java for version ${VERSION}" >> $GITHUB_STEP_SUMMARY
+ {
+ echo "### Site Deployment"
+ echo "Triggered deploy-site.yml on github/copilot-sdk-java for version ${VERSION}"
+ } >> "$GITHUB_STEP_SUMMARY"
env:
GITHUB_TOKEN: ${{ secrets.JAVA_RELEASE_GITHUB_TOKEN }}
diff --git a/.github/workflows/java-publish-snapshot.yml b/.github/workflows/java-publish-snapshot.yml
index 8c957627ff..3b67f7c1b1 100644
--- a/.github/workflows/java-publish-snapshot.yml
+++ b/.github/workflows/java-publish-snapshot.yml
@@ -16,8 +16,151 @@ concurrency:
cancel-in-progress: false
jobs:
- publish-snapshot:
+ resolve-source:
+ name: Resolve immutable snapshot source
+ runs-on: ubuntu-latest
+ outputs:
+ source_sha: ${{ steps.source.outputs.sha }}
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: ${{ github.sha }}
+ fetch-depth: 1
+ persist-credentials: false
+
+ - id: source
+ shell: bash
+ run: |
+ SHA=$(git rev-parse HEAD)
+ echo "sha=$SHA" >> "$GITHUB_OUTPUT"
+
+ build-windows-classifier:
+ name: Build Windows snapshot classifier
+ needs: resolve-source
+ runs-on: windows-latest
+ permissions:
+ contents: read
+ outputs:
+ version: ${{ steps.build.outputs.version }}
+ defaults:
+ run:
+ shell: pwsh
+ working-directory: ./java
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: ${{ github.sha }}
+ fetch-depth: 1
+ persist-credentials: false
+
+ - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
+ with:
+ java-version: "25"
+ distribution: "microsoft"
+ cache: "maven"
+
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ with:
+ node-version: 22
+
+ - name: Build and validate win32-x64 classifier
+ id: build
+ run: |
+ $sourceCommit = git rev-parse HEAD
+ if ($sourceCommit -ne '${{ needs.resolve-source.outputs.source_sha }}') {
+ throw "Checked out $sourceCommit instead of the resolved snapshot source."
+ }
+ node copilot-native/scripts/validate-native-host.mjs win32-x64
+ mvn -B -pl copilot-native package -DskipTests
+ $version = mvn help:evaluate "-Dexpression=project.version" -q "-DforceStdout"
+ $jar = "copilot-native/target/copilot-sdk-java-runtime-$version-win32-x64.jar"
+ $primaryJar = "copilot-native/target/copilot-sdk-java-runtime-$version.jar"
+ if (-not (Test-Path -LiteralPath $jar -PathType Leaf)) {
+ throw "Expected Windows classifier was not produced: $jar"
+ }
+ node copilot-native/scripts/validate-native-artifact.mjs classifier win32-x64 $jar ([IO.Path]::GetFileName($jar)) ..
+ node copilot-native/scripts/validate-native-artifact.mjs placeholder $primaryJar
+ $manifest = "copilot-native/target/win32-x64-$version.sha256"
+ $hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $jar).Hash.ToLowerInvariant()
+ "$hash $([IO.Path]::GetFileName($jar))" | Set-Content -NoNewline -Encoding ascii $manifest
+ node copilot-native/scripts/validate-native-artifact.mjs checksum $jar $manifest ([IO.Path]::GetFileName($jar))
+ "version=$version" | Add-Content $env:GITHUB_OUTPUT
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: java-native-win32-x64-snapshot-${{ github.run_id }}-${{ github.run_attempt }}
+ path: |
+ java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-win32-x64.jar
+ java/copilot-native/target/win32-x64-${{ steps.build.outputs.version }}.sha256
+ if-no-files-found: error
+ retention-days: 1
+
+ build-darwin-classifier:
+ name: Build Darwin snapshot classifier
+ needs: resolve-source
+ runs-on: macos-26
+ 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-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
+ with:
+ java-version: "25"
+ distribution: "microsoft"
+ cache: "maven"
+
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ with:
+ node-version: 22
+
+ - name: Build and validate darwin-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
+ node copilot-native/scripts/validate-native-host.mjs darwin-arm64
+ mvn -B -pl copilot-native package -DskipTests
+ VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
+ JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-darwin-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 darwin-arm64 "$JAR" "$(basename "$JAR")" ..
+ node copilot-native/scripts/validate-native-artifact.mjs placeholder "$PRIMARY_JAR"
+ MANIFEST="copilot-native/target/darwin-arm64-$VERSION.sha256"
+ HASH=$(shasum -a 256 "$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-darwin-arm64-snapshot-${{ github.run_id }}-${{ github.run_attempt }}
+ path: |
+ java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-darwin-arm64.jar
+ java/copilot-native/target/darwin-arm64-${{ steps.build.outputs.version }}.sha256
+ if-no-files-found: error
+ retention-days: 1
+
+ deploy-snapshot:
name: Publish SNAPSHOT to Maven Central
+ needs: [resolve-source, build-windows-classifier, build-darwin-classifier]
runs-on: ubuntu-latest
defaults:
run:
@@ -26,12 +169,13 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
- fetch-depth: 0
+ ref: ${{ github.sha }}
+ fetch-depth: 1
+ persist-credentials: false
- uses: ./.github/actions/setup-copilot
- - name: Set up JDK 25
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
+ - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: "25"
distribution: "microsoft"
@@ -40,22 +184,117 @@ jobs:
server-username: MAVEN_USERNAME
server-password: MAVEN_PASSWORD
- - name: Verify version is a SNAPSHOT
- working-directory: ./java
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ with:
+ node-version: 22
+
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: java-native-win32-x64-snapshot-${{ github.run_id }}-${{ github.run_attempt }}
+ path: ${{ runner.temp }}/java-native-win32-x64
+
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: java-native-darwin-arm64-snapshot-${{ github.run_id }}-${{ github.run_attempt }}
+ path: ${{ runner.temp }}/java-native-darwin-arm64
+
+ - name: Verify version, source, and Windows classifier
+ id: windows-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=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
- echo "Publishing version: $VERSION"
if [[ "$VERSION" != *"-SNAPSHOT" ]]; then
- echo "ERROR: This workflow only publishes SNAPSHOT versions. Current version: $VERSION"
+ echo "::error::This workflow only publishes SNAPSHOT versions. Current version: $VERSION"
exit 1
fi
- echo "### Snapshot Publish" >> $GITHUB_STEP_SUMMARY
- echo "- **Version:** $VERSION" >> $GITHUB_STEP_SUMMARY
- echo "- **Repository:** Maven Central Snapshots" >> $GITHUB_STEP_SUMMARY
+ if [ "$VERSION" != "${{ needs.build-windows-classifier.outputs.version }}" ]; then
+ echo "::error::Windows classifier version does not match deploy version."
+ exit 1
+ fi
+ ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-win32-x64"
+ JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-win32-x64.jar"
+ MANIFEST="$ARTIFACT_DIRECTORY/win32-x64-$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 win32-x64 "$JAR" "$(basename "$JAR")" "$GITHUB_WORKSPACE"
+ echo "windows_jar=$JAR" >> "$GITHUB_OUTPUT"
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+ echo "windows_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT"
- - name: Deploy Snapshot
- working-directory: ./java
- run: mvn -B deploy -DskipTests
+ - name: Verify version, source, and Darwin classifier
+ id: darwin-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.windows-artifact.outputs.version }}"
+ if [ "$VERSION" != "${{ needs.build-darwin-classifier.outputs.version }}" ]; then
+ echo "::error::Darwin classifier version does not match deploy version."
+ exit 1
+ fi
+ ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-darwin-arm64"
+ JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-darwin-arm64.jar"
+ MANIFEST="$ARTIFACT_DIRECTORY/darwin-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 darwin-arm64 "$JAR" "$(basename "$JAR")" "$GITHUB_WORKSPACE"
+ echo "darwin_jar=$JAR" >> "$GITHUB_OUTPUT"
+ echo "darwin_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT"
+
+ - name: Build Linux classifier and deploy complete snapshot
+ run: |
+ VERSION="${{ steps.windows-artifact.outputs.version }}"
+ mvn -B deploy -DskipTests -Dcopilot.native.libc=glibc \
+ "-Dcopilot.native.external.win32.classifier.path=${{ steps.windows-artifact.outputs.windows_jar }}" \
+ "-Dcopilot.native.external.darwin.classifier.path=${{ steps.darwin-artifact.outputs.darwin_jar }}"
+ LINUX_JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-linux-x64.jar"
+ test -f "$LINUX_JAR"
+ node copilot-native/scripts/validate-native-artifact.mjs \
+ classifier linux-x64 "$LINUX_JAR" "$(basename "$LINUX_JAR")" ..
+ LINUX_SHA=$(sha256sum "$LINUX_JAR" | cut -d ' ' -f 1)
+ GROUP_ID=$(mvn -q -pl copilot-native help:evaluate -Dexpression=project.groupId -DforceStdout)
+ ARTIFACT_ID=$(mvn -q -pl copilot-native help:evaluate -Dexpression=project.artifactId -DforceStdout)
+ POM_VERSION=$(mvn -q -pl copilot-native help:evaluate -Dexpression=project.version -DforceStdout)
+ if [ -z "$GROUP_ID" ] || [ -z "$ARTIFACT_ID" ] || [ "$POM_VERSION" != "$VERSION" ]; then
+ echo "::error::Unexpected copilot-native Maven coordinates: $GROUP_ID:$ARTIFACT_ID:$POM_VERSION (expected version $VERSION)"
+ exit 1
+ fi
+ {
+ echo "### Snapshot Publish"
+ echo "- **Version:** $VERSION"
+ echo "- **Source commit:** \`${{ needs.resolve-source.outputs.source_sha }}\`"
+ echo "- **Repository:** Maven Central Snapshots"
+ echo ""
+ echo "#### Maven Coordinates"
+ echo ""
+ echo '```xml'
+ echo ""
+ echo " $GROUP_ID"
+ echo " $ARTIFACT_ID"
+ echo " $POM_VERSION"
+ echo ""
+ echo '```'
+ echo ""
+ echo "#### Published Native Classifiers"
+ echo ""
+ echo "| Classifier | Build runner | Artifact | SHA-256 | Status |"
+ echo "| --- | --- | --- | --- | --- |"
+ echo "| \`linux-x64\` | \`ubuntu-latest\` | \`$(basename "$LINUX_JAR")\` | \`$LINUX_SHA\` | Published |"
+ echo "| \`win32-x64\` | \`windows-latest\` | \`$(basename "${{ steps.windows-artifact.outputs.windows_jar }}")\` | \`${{ steps.windows-artifact.outputs.windows_sha }}\` | Published |"
+ echo "| \`darwin-arm64\` | \`macos-26\` | \`$(basename "${{ steps.darwin-artifact.outputs.darwin_jar }}")\` | \`${{ steps.darwin-artifact.outputs.darwin_sha }}\` | Published |"
+ } >> "$GITHUB_STEP_SUMMARY"
env:
MAVEN_USERNAME: ${{ secrets.JAVA_MAVEN_CENTRAL_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.JAVA_MAVEN_CENTRAL_PASSWORD }}
diff --git a/.github/workflows/java-sdk-tests.yml b/.github/workflows/java-sdk-tests.yml
index bd0a34bd25..5f3d7377a0 100644
--- a/.github/workflows/java-sdk-tests.yml
+++ b/.github/workflows/java-sdk-tests.yml
@@ -18,12 +18,21 @@ permissions:
jobs:
java-sdk-inprocess:
- name: "Java SDK InProcess Tests"
+ name: "Java SDK InProcess Tests (${{ matrix.classifier }})"
if: github.event.repository.fork == false
- runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: ubuntu-latest
+ classifier: linux-x64
+ - os: windows-latest
+ classifier: win32-x64
+ - os: macos-26
+ classifier: darwin-arm64
+ runs-on: ${{ matrix.os }}
defaults:
run:
- shell: bash
working-directory: ./java
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -40,6 +49,9 @@ jobs:
with:
node-version: 22
+ - name: Validate native host
+ run: node copilot-native/scripts/validate-native-host.mjs ${{ matrix.classifier }}
+
- name: Run Java SDK tests (InProcess)
env:
CI: "true"
@@ -55,13 +67,201 @@ jobs:
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
- name: java-test-results-inprocess
+ name: java-test-results-inprocess-${{ matrix.classifier }}
path: |
java/sdk/target/surefire-reports/
java/sdk/target/surefire-reports-isolated/
java/sdk/target/failsafe-reports/
retention-days: 7
+ java-native-publication-windows:
+ name: "Java Native Publication Input (win32-x64)"
+ if: github.event.repository.fork == false
+ runs-on: windows-latest
+ permissions:
+ contents: read
+ outputs:
+ source_sha: ${{ steps.build.outputs.source_sha }}
+ version: ${{ steps.build.outputs.version }}
+ defaults:
+ run:
+ shell: pwsh
+ working-directory: ./java
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+
+ - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
+ with:
+ java-version: "25"
+ distribution: "microsoft"
+ cache: "maven"
+
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ with:
+ node-version: 22
+
+ - name: Build and validate win32-x64 classifier
+ id: build
+ run: |
+ node copilot-native/scripts/validate-native-host.mjs win32-x64
+ mvn -B -pl copilot-native package -DskipTests
+ $version = mvn help:evaluate "-Dexpression=project.version" -q "-DforceStdout"
+ $jar = "copilot-native/target/copilot-sdk-java-runtime-$version-win32-x64.jar"
+ $primaryJar = "copilot-native/target/copilot-sdk-java-runtime-$version.jar"
+ if (-not (Test-Path -LiteralPath $jar -PathType Leaf)) {
+ throw "Expected Windows classifier was not produced: $jar"
+ }
+ node copilot-native/scripts/validate-native-artifact.mjs classifier win32-x64 $jar ([IO.Path]::GetFileName($jar)) ..
+ node copilot-native/scripts/validate-native-artifact.mjs placeholder $primaryJar
+ $manifest = "copilot-native/target/win32-x64-$version.sha256"
+ $hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $jar).Hash.ToLowerInvariant()
+ "$hash $([IO.Path]::GetFileName($jar))" | Set-Content -NoNewline -Encoding ascii $manifest
+ node copilot-native/scripts/validate-native-artifact.mjs checksum $jar $manifest ([IO.Path]::GetFileName($jar))
+ echo "source_sha=$(git rev-parse HEAD)" >> $env:GITHUB_OUTPUT
+ echo "version=$version" >> $env:GITHUB_OUTPUT
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: java-native-publication-win32-x64-${{ github.run_id }}-${{ github.run_attempt }}
+ path: |
+ java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-win32-x64.jar
+ java/copilot-native/target/win32-x64-${{ steps.build.outputs.version }}.sha256
+ if-no-files-found: error
+ retention-days: 1
+
+ java-native-publication-darwin:
+ name: "Java Native Publication Input (darwin-arm64)"
+ if: github.event.repository.fork == false
+ runs-on: macos-26
+ permissions:
+ contents: read
+ outputs:
+ source_sha: ${{ steps.build.outputs.source_sha }}
+ version: ${{ steps.build.outputs.version }}
+ defaults:
+ run:
+ shell: bash
+ working-directory: ./java
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+
+ - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
+ with:
+ java-version: "25"
+ distribution: "microsoft"
+ cache: "maven"
+
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ with:
+ node-version: 22
+
+ - name: Build and validate darwin-arm64 classifier
+ id: build
+ run: |
+ set -euo pipefail
+ node copilot-native/scripts/validate-native-host.mjs darwin-arm64
+ mvn -B -pl copilot-native package -DskipTests
+ VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
+ JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-darwin-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 darwin-arm64 "$JAR" "$(basename "$JAR")" ..
+ node copilot-native/scripts/validate-native-artifact.mjs placeholder "$PRIMARY_JAR"
+ MANIFEST="copilot-native/target/darwin-arm64-$VERSION.sha256"
+ HASH=$(shasum -a 256 "$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 "source_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: java-native-publication-darwin-arm64-${{ github.run_id }}-${{ github.run_attempt }}
+ path: |
+ java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-darwin-arm64.jar
+ java/copilot-native/target/darwin-arm64-${{ steps.build.outputs.version }}.sha256
+ if-no-files-found: error
+ retention-days: 1
+
+ java-native-publication-assembly:
+ name: "Java Native Publication Assembly"
+ if: github.event.repository.fork == false
+ needs: [java-native-publication-windows, java-native-publication-darwin]
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ working-directory: ./java
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: ${{ github.sha }}
+ persist-credentials: false
+
+ - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
+ with:
+ java-version: "25"
+ distribution: "microsoft"
+ cache: "maven"
+
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ with:
+ node-version: 22
+
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: java-native-publication-win32-x64-${{ github.run_id }}-${{ github.run_attempt }}
+ path: ${{ github.workspace }}/java/native-publication-input/windows
+
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: java-native-publication-darwin-arm64-${{ github.run_id }}-${{ github.run_attempt }}
+ path: ${{ github.workspace }}/java/native-publication-input/darwin
+
+ - name: Verify native inputs and deploy the complete local release
+ run: |
+ set -euo pipefail
+ test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-windows.outputs.source_sha }}"
+ test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-darwin.outputs.source_sha }}"
+ VERSION="${{ needs.java-native-publication-windows.outputs.version }}"
+ test "$VERSION" = "${{ needs.java-native-publication-darwin.outputs.version }}"
+ WINDOWS_DIRECTORY="$GITHUB_WORKSPACE/java/native-publication-input/windows"
+ DARWIN_DIRECTORY="$GITHUB_WORKSPACE/java/native-publication-input/darwin"
+ WINDOWS_JAR="$WINDOWS_DIRECTORY/copilot-sdk-java-runtime-$VERSION-win32-x64.jar"
+ WINDOWS_MANIFEST="$WINDOWS_DIRECTORY/win32-x64-$VERSION.sha256"
+ DARWIN_JAR="$DARWIN_DIRECTORY/copilot-sdk-java-runtime-$VERSION-darwin-arm64.jar"
+ DARWIN_MANIFEST="$DARWIN_DIRECTORY/darwin-arm64-$VERSION.sha256"
+ node copilot-native/scripts/validate-native-artifact.mjs \
+ checksum "$WINDOWS_JAR" "$WINDOWS_MANIFEST" "$(basename "$WINDOWS_JAR")"
+ node copilot-native/scripts/validate-native-artifact.mjs \
+ classifier win32-x64 "$WINDOWS_JAR" "$(basename "$WINDOWS_JAR")" ..
+ node copilot-native/scripts/validate-native-artifact.mjs \
+ checksum "$DARWIN_JAR" "$DARWIN_MANIFEST" "$(basename "$DARWIN_JAR")"
+ node copilot-native/scripts/validate-native-artifact.mjs \
+ classifier darwin-arm64 "$DARWIN_JAR" "$(basename "$DARWIN_JAR")" ..
+ export GNUPGHOME="$GITHUB_WORKSPACE/java/copilot-native/target/local-publication-gpg"
+ rm -rf "$GNUPGHOME"
+ mkdir -p "$GNUPGHOME"
+ chmod 700 "$GNUPGHOME"
+ gpg --batch --pinentry-mode loopback --passphrase '' \
+ --quick-generate-key 'Copilot SDK local validation ' rsa2048 sign 1d
+ LOCAL_REPOSITORY="$GITHUB_WORKSPACE/java/copilot-native/target/local-publication-repository"
+ rm -rf "$LOCAL_REPOSITORY"
+ mvn -B -pl copilot-native deploy -Prelease -DskipTests \
+ -Dcopilot.native.libc=glibc \
+ -Dcopilot.native.test.local.publication=true \
+ "-Dcopilot.native.external.win32.classifier.path=$WINDOWS_JAR" \
+ "-Dcopilot.native.external.darwin.classifier.path=$DARWIN_JAR" \
+ "-Dmaven.repo.local=$LOCAL_REPOSITORY"
+ node copilot-native/scripts/validate-local-publication.mjs \
+ "$LOCAL_REPOSITORY" copilot-sdk-java-runtime "$VERSION" .. --signatures
+
java-sdk:
name: "Java SDK Tests (JDK ${{ matrix.test-jdk }})"
if: github.event.repository.fork == false
@@ -125,7 +325,9 @@ jobs:
if: matrix.test-jdk == '25'
env:
CI: "true"
- run: mvn verify -Dskip.test.harness=true
+ run: |
+ node copilot-native/scripts/validate-native-host.mjs linux-x64
+ mvn verify -Dskip.test.harness=true -Dcopilot.native.libc=glibc -Dcopilot.native.skip.download=false
- name: Switch to JDK 17
if: matrix.test-jdk == '17'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a5974c67d7..d695ed6fb6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,14 @@ See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the fu
## [Unreleased]
+### Feature: rotating session-scoped GitHub credentials
+
+All six SDKs can now acquire short-lived GitHub credentials through a session-scoped callback. The SDK registers the callback before session create or resume, maps `initial` and `refresh` requests to the owning session, and removes registrations on rollback, replacement, session close, and client close. Static per-session `gitHubToken` credentials remain supported and are mutually exclusive with the callback.
+
+Token responses use the shared tagged token/cancelled shape and require `expiresIn`, expressed as the positive number of seconds remaining when the callback completes. See [github/copilot-agent-runtime#16381](https://github.com/github/copilot-agent-runtime/pull/16381) for the runtime credential-authority implementation.
+
+Initial acquisition occurs during create or resume; cancellation, callback errors, and invalid credentials reject that operation instead of falling back to ambient authentication. Idle sessions refresh only before their next credential-consuming operation.
+
### Feature: extensions can request sensitive environment variables
Copilot CLI extensions can now ask for named sensitive environment variables when they join a session. `joinSession()` accepts a `requestedEnvironmentVariables` option listing the variable names the extension needs. The CLI shows a permission prompt naming the extension and the exact variables requested. On approval, only those variables reach that extension and their values are written into the extension process's `process.env` before `joinSession()` resolves. On denial, `joinSession()` rejects, the extension does not load, and its tools never reach the model.
diff --git a/docs/auth/authenticate.md b/docs/auth/authenticate.md
index 2fc80dd0aa..a7582b5510 100644
--- a/docs/auth/authenticate.md
+++ b/docs/auth/authenticate.md
@@ -262,6 +262,131 @@ const client = new CopilotClient({
For more information, see [GitHub OAuth](../setup/github-oauth.md).
+## Rotating session-scoped GitHub tokens
+
+For multi-user services and integrations, set a token provider on each session instead of storing one long-lived token. The runtime calls the provider for the effective GitHub host and identifies the request as `initial` or `refresh`. The session ID is absent only when a cloud session has not received its ID yet.
+
+Return a tagged token result or an explicit cancellation. Every token result must include `expiresIn`: the positive number of seconds remaining when the callback completes. Production GitHub tokens typically last eight hours, so `8 * 60 * 60` is a common value. Do not set both the static per-session token and the provider.
+
+
+TypeScript
+
+
+```typescript
+const session = await client.createSession({
+ gitHubTokenProvider: async ({ host, sessionId, reason }) => {
+ const token = await acquireGitHubToken({ host, sessionId, reason });
+ return {
+ kind: "token",
+ accessToken: token.value,
+ expiresIn: token.secondsRemaining,
+ };
+ },
+});
+```
+
+
+
+Python
+
+
+```python
+async def provide_github_token(args):
+ token = await acquire_github_token(
+ host=args["host"],
+ session_id=args["session_id"],
+ reason=args["reason"],
+ )
+ return {
+ "kind": "token",
+ "accessToken": token.value,
+ "expiresIn": token.seconds_remaining,
+ }
+
+
+session = await client.create_session(github_token_provider=provide_github_token)
+```
+
+
+
+Go
+
+
+```go
+session, err := client.CreateSession(ctx, &copilot.SessionConfig{
+ GitHubTokenProvider: func(args copilot.GitHubTokenProviderArgs) (*copilot.GitHubTokenProviderResult, error) {
+ token, secondsRemaining, err := acquireGitHubToken(args.Host, args.SessionID, args.Reason)
+ if err != nil {
+ return nil, err
+ }
+ return copilot.GitHubTokenResult(&copilot.GitHubToken{
+ AccessToken: token,
+ ExpiresIn: secondsRemaining,
+ }), nil
+ },
+})
+```
+
+
+
+.NET
+
+
+```csharp
+await using var session = await client.CreateSessionAsync(new SessionConfig
+{
+ GitHubTokenProvider = async args =>
+ {
+ var token = await AcquireGitHubTokenAsync(args.Host, args.SessionId, args.Reason);
+ return GitHubTokenProviderResult.FromToken(new GitHubToken
+ {
+ AccessToken = token.Value,
+ ExpiresIn = token.SecondsRemaining,
+ });
+ },
+});
+```
+
+
+
+Java
+
+
+```java
+var session = client.createSession(new SessionConfig()
+ .setGitHubTokenProvider(args ->
+ acquireGitHubToken(args.host(), args.sessionId(), args.reason())
+ .thenApply(token -> GitHubTokenProviderResult.token(
+ token.value(), token.secondsRemaining())))
+ .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
+).get();
+```
+
+
+
+Rust
+
+
+```rust
+let provider = Arc::new(|args: GitHubTokenProviderArgs| async move {
+ let token = acquire_github_token(&args.host, args.session_id.as_ref(), args.reason).await?;
+ Ok(GitHubTokenProviderResult::Token(GitHubToken::new(
+ token.value,
+ token.seconds_remaining,
+ )))
+});
+
+let session = client
+ .create_session(SessionConfig::default().with_github_token_provider(provider))
+ .await?;
+```
+
+
+
+The runtime performs the `initial` acquisition as part of session creation or resume. A cancelled acquisition, provider error, invalid response, or token without a stable account identity rejects the create or resume operation. The runtime does not fall back to ambient authentication.
+
+After the session is established, the runtime performs async preflight before each credential-consuming operation. It requests a `refresh` when the current token has one hour or less remaining. Idle sessions are not refreshed until their next credential-consuming operation. The runtime does not use background timers, rejection-driven replay, 401/403 challenge propagation, or upscope for this callback.
+
## Environment variables
For automation, CI/CD pipelines, and server-to-server scenarios, you can authenticate using environment variables.
diff --git a/docs/features/skills.md b/docs/features/skills.md
index 5b8388162a..d76196429e 100644
--- a/docs/features/skills.md
+++ b/docs/features/skills.md
@@ -351,6 +351,22 @@ The markdown body contains the instructions that are injected into the session c
| .NET | `SkillDirectories` | `List` | Directories to load skills from |
| .NET | `DisabledSkills` | `List` | Skills to disable |
+### Built-in skills and `mode: "empty"`
+
+The runtime ships with a set of bundled **built-in** skills that are eligible by
+default. When you run the client in `mode: "empty"` (the recommended baseline for
+[multi-tenant servers](../setup/multi-tenancy.md)), the SDK excludes every
+runtime-bundled built-in skill: it sends an empty `includedBuiltinSkills` list on
+the post-create and post-resume options patch, alongside the empty
+`installedPlugins` list.
+
+This exclusion is the default, not a permanent restriction. To allow selected
+runtime-bundled skills, set `includedBuiltinSkills` (or the language-specific
+casing) to their names. You can also opt into your **own** custom skills under
+`mode: "empty"`—enable skills and pass your own `skillDirectories`—and those
+remain fully usable, including a custom skill that shares a name with a built-in.
+Under `mode: "copilot-cli"` the field is omitted unless you set the option.
+
## Best practices
1. **Organize by domain** - Group related skills together (e.g., `skills/security/`, `skills/testing/`)
diff --git a/docs/setup/bundled-cli.md b/docs/setup/bundled-cli.md
index f067de8fde..9d9c80c35b 100644
--- a/docs/setup/bundled-cli.md
+++ b/docs/setup/bundled-cli.md
@@ -39,6 +39,9 @@ flowchart TB
## Quick start
+> [!NOTE]
+> Each SDK has a minimum language runtime requirement—see the Prerequisites section of the [Node.js](../../nodejs/README.md#prerequisites), [Python](../../python/README.md#prerequisites), [Go](../../go/README.md#prerequisites), [Rust](../../rust/README.md#prerequisites), [Java](../../java/README.md#prerequisites), or [.NET](../../dotnet/README.md#prerequisites) README—since an unsupported runtime (e.g. Python below the stated floor) can cause `pip`/package managers to silently resolve an outdated SDK release instead of reporting a version conflict.
+
Node.js / TypeScript
diff --git a/docs/setup/multi-tenancy.md b/docs/setup/multi-tenancy.md
index 2f82dde0bf..ebbafe4fb0 100644
--- a/docs/setup/multi-tenancy.md
+++ b/docs/setup/multi-tenancy.md
@@ -24,7 +24,9 @@ This guide is a sister to [Scaling and multi-tenancy](./scaling.md). Use that gu
| `baseDirectory` | Isolating `COPILOT_HOME` per runtime instance | Ignored when connecting to an existing runtime. |
| `sessionFs` | Routing session filesystem storage off local disk | Pair with per-session filesystem providers. |
| `RuntimeConnection.forUri(url)` | Sharing one already-running runtime | Language names vary; see samples below. |
-| Per-session `gitHubToken` | Scoping auth to the requesting user | Prefer this over a single shared user token. |
+| Per-session GitHub token or provider | Scoping auth to the requesting user | Prefer a rotating provider for short-lived credentials; use a static `gitHubToken` only when rotation is unnecessary. |
+
+For callback-backed credentials, see [Rotating session-scoped GitHub tokens](../auth/authenticate.md#rotating-session-scoped-github-tokens). Each session owns its provider registration, so concurrent sessions can use different GitHub hosts and accounts without sharing callback state.
### `mode: "empty"`
@@ -396,10 +398,13 @@ Session-level isolation means the runtime keeps user-specific model and state in
| Session state | Per session ID under `COPILOT_HOME/session-state/{sessionId}`. |
| GitHub identity | Per-session when `gitHubToken` is set on the session. |
| Tools | Explicit in `mode: "empty"`; ambient in `mode: "copilot-cli"`. |
+| Skills | In `mode: "empty"` no runtime-bundled built-in skills are eligible by default; callers can allow selected built-ins or opt into their own custom skills. Ambient in `mode: "copilot-cli"`. |
| Host filesystem | Shared by the runtime process if host tools are available. |
`mode: "empty"` is what makes shared runtime patterns viable: no ambient OS tools are exposed unless your application registers or allows them. With `mode: "copilot-cli"`, OS filesystem access is shared through the host process, so do not use that mode for multi-user server mode.
+Under `mode: "empty"` the SDK excludes every runtime-bundled built-in skill by default (it sends an empty `includedBuiltinSkills` list on the post-create/post-resume options patch, alongside the empty `installedPlugins` list). Set `includedBuiltinSkills` (or the language-specific casing) to explicitly allow selected built-ins, just as `availableTools` allows selected runtime-bundled tools. A caller can also opt into its **own** custom skills—for example by enabling skills and pointing at its own skill directories—and those remain usable.
+
Session state is stored under `COPILOT_HOME/session-state/{sessionId}` unless you route it through `sessionFs`. Use unique session IDs that include your own tenant or user boundary, and enforce access control before resuming or deleting sessions.
## Pattern comparison
diff --git a/dotnet/README.md b/dotnet/README.md
index 6efd6e094c..461ff0cf94 100644
--- a/dotnet/README.md
+++ b/dotnet/README.md
@@ -133,6 +133,7 @@ Create a new conversation session.
- `InfiniteSessions` - Configure automatic context compaction (see below)
- `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`.
- `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 user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section.
- `Hooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
@@ -144,6 +145,24 @@ Resume an existing session. Returns the session with `WorkspacePath` populated i
**ResumeSessionConfig:**
- `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`.
+
+```csharp
+await using var session = await client.CreateSessionAsync(new SessionConfig
+{
+ GitHubTokenProvider = async args =>
+ {
+ var token = await AcquireTokenAsync(args.Host);
+ return GitHubTokenProviderResult.FromToken(new GitHubToken
+ {
+ AccessToken = token,
+ ExpiresIn = 8 * 60 * 60
+ });
+ }
+});
+```
+
+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.
##### `PingAsync(string? message = null): Task`
diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs
index 86178ba74b..f2da0a48f7 100644
--- a/dotnet/src/Client.cs
+++ b/dotnet/src/Client.cs
@@ -70,6 +70,7 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
/// that has not been explicitly disposed or removed.
///
internal readonly ConcurrentDictionary _sessions = new();
+ private readonly ConcurrentDictionary>> _gitHubTokenProviders = new();
private readonly CopilotClientOptions _options;
private readonly RuntimeConnection _connection;
@@ -91,8 +92,7 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
///
/// Client-global RPC handlers (e.g. the LLM inference provider adapter),
- /// built once at construction when the corresponding option is configured and
- /// registered on every connection. Null when no client-global API is enabled.
+ /// built once at construction and registered on every connection.
///
private readonly ClientGlobalApiHandlers? _clientGlobalApis;
@@ -541,6 +541,7 @@ public async Task StopAsync()
}
_sessions.Clear();
+ ClearGitHubTokenProviders();
await CleanupConnectionAsync(errors, gracefulRuntimeShutdown: true);
@@ -572,6 +573,7 @@ public async Task StopAsync()
public async Task ForceStopAsync()
{
_sessions.Clear();
+ ClearGitHubTokenProviders();
var errors = new List();
await CleanupConnectionAsync(errors, gracefulRuntimeShutdown: false);
@@ -1030,8 +1032,9 @@ private void ApplyConfigDefaultsForMode(SessionConfigBase config)
/// patch for the current mode. In empty mode this defaults the four
/// overridable feature flags to safe values (caller values from
/// win); installedPlugins=[] is
- /// unconditional under empty mode so apps that need plugins must switch
- /// modes. In copilot-cli mode only explicitly-set fields are forwarded.
+ /// unconditional under empty mode. includedBuiltinSkills defaults to
+ /// an empty list, but callers can explicitly allow selected runtime-bundled
+ /// skills. In copilot-cli mode only explicitly-set fields are forwarded.
///
private async Task UpdateSessionOptionsForModeAsync(CopilotSession session, SessionConfigBase config, CancellationToken cancellationToken)
{
@@ -1041,6 +1044,7 @@ private async Task UpdateSessionOptionsForModeAsync(CopilotSession session, Sess
bool? coauthorEnabled = null;
bool? manageScheduleEnabled = null;
IList? installedPlugins = null;
+ IList? includedBuiltinSkills = null;
if (_options.Mode == CopilotClientMode.Empty)
{
@@ -1049,6 +1053,7 @@ private async Task UpdateSessionOptionsForModeAsync(CopilotSession session, Sess
coauthorEnabled = config.CoauthorEnabled ?? false;
manageScheduleEnabled = config.ManageScheduleEnabled ?? false;
installedPlugins = [];
+ includedBuiltinSkills = config.IncludedBuiltinSkills ?? [];
hasAnyPatch = true;
}
else
@@ -1057,6 +1062,7 @@ private async Task UpdateSessionOptionsForModeAsync(CopilotSession session, Sess
if (config.CustomAgentsLocalOnly is not null) { customAgentsLocalOnly = config.CustomAgentsLocalOnly; hasAnyPatch = true; }
if (config.CoauthorEnabled is not null) { coauthorEnabled = config.CoauthorEnabled; hasAnyPatch = true; }
if (config.ManageScheduleEnabled is not null) { manageScheduleEnabled = config.ManageScheduleEnabled; hasAnyPatch = true; }
+ if (config.IncludedBuiltinSkills is not null) { includedBuiltinSkills = config.IncludedBuiltinSkills; hasAnyPatch = true; }
}
if (!hasAnyPatch) return;
@@ -1070,6 +1076,7 @@ await session.Rpc.Options.UpdateAsync(
coauthorEnabled: coauthorEnabled,
manageScheduleEnabled: manageScheduleEnabled,
installedPlugins: installedPlugins,
+ includedBuiltinSkills: includedBuiltinSkills,
cancellationToken: cancellationToken).ConfigureAwait(false);
#pragma warning restore GHCP001
}
@@ -1119,6 +1126,7 @@ await session.Rpc.Options.UpdateAsync(
public async Task CreateSessionAsync(SessionConfig config, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(config);
+ ValidateGitHubTokenConfig(config);
var connection = await EnsureConnectedAsync(cancellationToken);
var totalTimestamp = Stopwatch.GetTimestamp();
@@ -1153,19 +1161,22 @@ public async Task CreateSessionAsync(SessionConfig config, Cance
? null
: (string.IsNullOrEmpty(config.SessionId) ? Guid.NewGuid().ToString() : config.SessionId);
+ var registrationId = RegisterGitHubTokenProvider(config.GitHubTokenProvider);
+ var registrationTransferred = false;
CopilotSession? session = null;
- if (localSessionId != null)
- {
- session = InitializeSession(
- localSessionId,
- connection.Rpc,
- config,
- transformCallbacks,
- hasHooks,
- "CopilotClient.CreateSessionAsync");
- }
try
{
+ if (localSessionId != null)
+ {
+ session = InitializeSession(
+ localSessionId,
+ connection.Rpc,
+ config,
+ transformCallbacks,
+ hasHooks,
+ "CopilotClient.CreateSessionAsync");
+ }
+
var (traceparent, tracestate) = TelemetryHelpers.GetTraceContext();
var request = new CreateSessionRequest(
@@ -1222,6 +1233,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance
Tracestate: tracestate,
ModelCapabilities: config.ModelCapabilities,
GitHubToken: config.GitHubToken,
+ GitHubTokenProviderRegistrationId: registrationId,
RemoteSession: config.RemoteSession,
Cloud: config.Cloud,
InstructionDirectories: config.InstructionDirectories,
@@ -1301,6 +1313,11 @@ public async Task CreateSessionAsync(SessionConfig config, Cance
session.SetOpenCanvases(response.OpenCanvases);
await UpdateSessionOptionsForModeAsync(session, config, cancellationToken).ConfigureAwait(false);
+ if (registrationId is not null)
+ {
+ session.SetGitHubTokenProviderRegistration(registrationId);
+ registrationTransferred = true;
+ }
}
catch (Exception ex)
{
@@ -1316,6 +1333,13 @@ public async Task CreateSessionAsync(SessionConfig config, Cance
throw;
}
+ finally
+ {
+ if (!registrationTransferred && registrationId is not null)
+ {
+ UnregisterGitHubTokenProvider(registrationId);
+ }
+ }
LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null,
"CopilotClient.CreateSessionAsync complete. Elapsed={Elapsed}, SessionId={SessionId}",
@@ -1353,6 +1377,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes
{
ArgumentNullException.ThrowIfNull(sessionId);
ArgumentNullException.ThrowIfNull(config);
+ ValidateGitHubTokenConfig(config);
var connection = await EnsureConnectedAsync(cancellationToken);
var totalTimestamp = Stopwatch.GetTimestamp();
@@ -1375,17 +1400,21 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes
var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage);
- // Create and register the session before issuing the RPC so that
- // events emitted by the CLI (e.g. session.start) are not dropped.
- var session = InitializeSession(
- sessionId,
- connection.Rpc,
- config,
- transformCallbacks,
- hasHooks,
- "CopilotClient.ResumeSessionAsync");
+ var registrationId = RegisterGitHubTokenProvider(config.GitHubTokenProvider);
+ var registrationTransferred = false;
+ CopilotSession? session = null;
try
{
+ // Create and register the session before issuing the RPC so that
+ // events emitted by the CLI (e.g. session.start) are not dropped.
+ session = InitializeSession(
+ sessionId,
+ connection.Rpc,
+ config,
+ transformCallbacks,
+ hasHooks,
+ "CopilotClient.ResumeSessionAsync");
+
var (traceparent, tracestate) = TelemetryHelpers.GetTraceContext();
var request = new ResumeSessionRequest(
@@ -1443,6 +1472,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes
Tracestate: tracestate,
ModelCapabilities: config.ModelCapabilities,
GitHubToken: config.GitHubToken,
+ GitHubTokenProviderRegistrationId: registrationId,
RemoteSession: config.RemoteSession,
ContinuePendingWork: config.ContinuePendingWork,
InstructionDirectories: config.InstructionDirectories,
@@ -1486,10 +1516,15 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes
}
await UpdateSessionOptionsForModeAsync(session, config, cancellationToken).ConfigureAwait(false);
+ if (registrationId is not null)
+ {
+ session.SetGitHubTokenProviderRegistration(registrationId);
+ registrationTransferred = true;
+ }
}
catch (Exception ex)
{
- session.RemoveFromClient();
+ session?.RemoveFromClient();
if (ex is not OperationCanceledException)
{
LoggingHelpers.LogTiming(_logger, LogLevel.Warning, ex,
@@ -1499,12 +1534,19 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes
}
throw;
}
+ finally
+ {
+ if (!registrationTransferred && registrationId is not null)
+ {
+ UnregisterGitHubTokenProvider(registrationId);
+ }
+ }
LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null,
"CopilotClient.ResumeSessionAsync complete. Elapsed={Elapsed}, SessionId={SessionId}",
totalTimestamp,
sessionId);
- return session;
+ return session!;
}
///
@@ -1664,7 +1706,10 @@ public async Task DeleteSessionAsync(string sessionId, CancellationToken cancell
throw new InvalidOperationException($"Failed to delete session {sessionId}: {response.Error}");
}
- RemoveSession(sessionId);
+ if (_sessions.TryRemove(sessionId, out var session))
+ {
+ session.ReleaseGitHubTokenProviderRegistration();
+ }
}
///
@@ -1946,25 +1991,94 @@ await Rpc.SessionFs.SetProviderAsync(
///
/// Builds the client-global RPC handler bag at construction time. Registers
/// the LLM inference provider adapter and/or the GitHub telemetry adapter
- /// depending on which options are configured; returns null when no
- /// client-global API is configured so the registration is skipped entirely.
+ /// depending on which options are configured. The GitHub token dispatcher is
+ /// always registered because providers are configured per session.
///
private ClientGlobalApiHandlers? BuildClientGlobalApis()
{
var handler = _options.RequestHandler;
var onGitHubTelemetry = _options.OnGitHubTelemetry;
- if (handler is null && onGitHubTelemetry is null)
- {
- return null;
- }
-
return new ClientGlobalApiHandlers
{
LlmInference = handler is null ? null : new LlmInferenceAdapter(handler, () => _serverRpc),
GitHubTelemetry = onGitHubTelemetry is null ? null : new GitHubTelemetryAdapter(onGitHubTelemetry, _logger),
+ GitHubToken = new GitHubTokenAdapter(this),
};
}
+ private static void ValidateGitHubTokenConfig(SessionConfigBase config)
+ {
+ if (config.GitHubToken is not null && config.GitHubTokenProvider is not null)
+ {
+ throw new ArgumentException(
+ $"{nameof(SessionConfigBase.GitHubToken)} and {nameof(SessionConfigBase.GitHubTokenProvider)} cannot be used together.",
+ nameof(config));
+ }
+ }
+
+ private string? RegisterGitHubTokenProvider(
+ Func>? provider)
+ {
+ if (provider is null)
+ {
+ return null;
+ }
+
+ var registrationId = Guid.NewGuid().ToString();
+ if (!_gitHubTokenProviders.TryAdd(registrationId, provider))
+ {
+ throw new InvalidOperationException("Failed to register GitHub token provider.");
+ }
+ return registrationId;
+ }
+
+ internal void UnregisterGitHubTokenProvider(string registrationId)
+ => _gitHubTokenProviders.TryRemove(registrationId, out _);
+
+ private void ClearGitHubTokenProviders() => _gitHubTokenProviders.Clear();
+
+ private sealed class GitHubTokenAdapter(CopilotClient client) : IGitHubTokenHandler
+ {
+ public async Task GetTokenAsync(
+ GitHubTokenAcquireRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ if (!client._gitHubTokenProviders.TryGetValue(request.RegistrationId, out var provider))
+ {
+ throw new InvalidOperationException(
+ $"Unknown GitHub token provider registration ID '{request.RegistrationId}'.");
+ }
+
+ var reason = request.Reason == GitHubTokenAcquireReason.Initial
+ ? GitHubTokenRequestReason.Initial
+ : request.Reason == GitHubTokenAcquireReason.Refresh
+ ? GitHubTokenRequestReason.Refresh
+ : throw new InvalidOperationException($"Unknown GitHub token request reason '{request.Reason}'.");
+ var result = await provider(new GitHubTokenProviderArgs
+ {
+ Host = request.Host,
+ SessionId = request.SessionId,
+ Reason = reason,
+ }).ConfigureAwait(false);
+
+ if (result is { Cancelled: true })
+ {
+ return new GitHubTokenAcquireResultCancelled();
+ }
+ if (result?.Token is not { } token)
+ {
+ throw new InvalidOperationException(
+ "GitHub token provider returned neither a token nor cancellation.");
+ }
+ return new GitHubTokenAcquireResultToken
+ {
+ AccessToken = token.AccessToken,
+ TokenType = token.TokenType,
+ ExpiresIn = token.ExpiresIn,
+ };
+ }
+ }
+
///
/// Tells the runtime to route its outbound model-layer requests through this
/// client's LLM inference provider. No-op when interception is not configured.
@@ -2542,11 +2656,6 @@ private void RegisterSession(CopilotSession session)
}
}
- private void RemoveSession(string sessionId)
- {
- _sessions.TryRemove(sessionId, out _);
- }
-
///
/// Disposes the synchronously.
///
@@ -2802,6 +2911,7 @@ internal record CreateSessionRequest(
string? Tracestate = null,
ModelCapabilitiesOverride? ModelCapabilities = null,
string? GitHubToken = null,
+ [property: JsonPropertyName("gitHubTokenProviderRegistrationId")] string? GitHubTokenProviderRegistrationId = null,
RemoteSessionMode? RemoteSession = null,
CloudSessionOptions? Cloud = null,
IList? InstructionDirectories = null,
@@ -2917,6 +3027,7 @@ internal record ResumeSessionRequest(
string? Tracestate = null,
ModelCapabilitiesOverride? ModelCapabilities = null,
string? GitHubToken = null,
+ [property: JsonPropertyName("gitHubTokenProviderRegistrationId")] string? GitHubTokenProviderRegistrationId = null,
RemoteSessionMode? RemoteSession = null,
bool? ContinuePendingWork = null,
IList? InstructionDirectories = null,
diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs
index 97f9b52762..ba8b844d83 100644
--- a/dotnet/src/Generated/Rpc.cs
+++ b/dotnet/src/Generated/Rpc.cs
@@ -61,10 +61,35 @@ internal sealed class ConnectResult
public string Version { get; set; } = string.Empty;
}
+/// Identity of the integrating host, declared once on the `server.connect` handshake so telemetry from this connection is attributed to a single, consistent surface. All fields are optional; omit them to keep the default attribution.
+[Experimental(Diagnostics.Experimental)]
+internal sealed class ConnectClientInfo
+{
+ /// Name of the host editor, e.g. `"vscode"`.
+ [JsonPropertyName("editorName")]
+ public string? EditorName { get; set; }
+
+ /// Version of the host editor, e.g. `"1.124.2"`. Ignored unless it looks like a version string.
+ [JsonPropertyName("editorVersion")]
+ public string? EditorVersion { get; set; }
+
+ /// Name of the Copilot extension within the host, e.g. `"copilot-chat"`.
+ [JsonPropertyName("extensionName")]
+ public string? ExtensionName { get; set; }
+
+ /// Version of the Copilot extension within the host, e.g. `"0.54.0"`. Ignored unless it looks like a version string.
+ [JsonPropertyName("extensionVersion")]
+ public string? ExtensionVersion { get; set; }
+}
+
/// Connection-level opt-ins for the `server.connect` handshake. Transport authentication is consumed by the native protocol boundary before dispatch.
[Experimental(Diagnostics.Experimental)]
internal sealed class ConnectRequest
{
+ /// Identity of the integrating host. Optional; omit it to keep the default attribution.
+ [JsonPropertyName("clientInfo")]
+ public ConnectClientInfo? ClientInfo { get; set; }
+
/// Opt this connection in to GitHub telemetry forwarding for its lifetime. When set, the runtime forwards every internal telemetry event it emits — across all sessions, plus sessionless events — to this connection over the `gitHubTelemetry.event` notification. Regular events are also written to the runtime's normal GitHub/CTS path (dual-write); host-only compatibility events are forward-only and intentionally skip that path. Intended for first-party hosts that re-emit the events into their own telemetry stores. Both unrestricted and restricted events are forwarded, each tagged with a `restricted` discriminator; a backstop drops restricted events when restricted telemetry is disabled — using the process-global gate for ordinary events and an explicit session-scoped decision for host-only events.
[JsonPropertyName("enableGitHubTelemetryForwarding")]
public bool? EnableGitHubTelemetryForwarding { get; set; }
@@ -282,6 +307,19 @@ public sealed class ModelCapabilities
public ModelCapabilitiesSupports? Supports { get; set; }
}
+/// A service-published message about a model, carrying a stable machine-readable code alongside human-readable text.
+[Experimental(Diagnostics.Experimental)]
+public sealed class ModelMessage
+{
+ /// Stable machine-readable identifier for the message, such as `client_version_deprecated`. Hosts can key custom presentation off this; unrecognized codes should fall back to displaying `message`.
+ [JsonPropertyName("code")]
+ public string Code { get; set; } = string.Empty;
+
+ /// Human-readable message text intended for display to the user.
+ [JsonPropertyName("message")]
+ public string Message { get; set; } = string.Empty;
+}
+
/// Policy state (if applicable).
[Experimental(Diagnostics.Experimental)]
public sealed class ModelPolicy
@@ -295,6 +333,15 @@ public sealed class ModelPolicy
public string? Terms { get; set; }
}
+/// Service-published warning text that hosts should display when presenting a model.
+[Experimental(Diagnostics.Experimental)]
+public sealed class ModelWarningText
+{
+ /// Data-retention warning for the model. The text may contain Markdown links and should be rendered as Markdown when supported.
+ [JsonPropertyName("dataRetention")]
+ public string? DataRetention { get; set; }
+}
+
/// Copilot model metadata, including identifier, display name, capabilities, policy, billing, reasoning efforts, and picker categories.
[Experimental(Diagnostics.Experimental)]
public sealed class Model
@@ -315,6 +362,10 @@ public sealed class Model
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
+ /// Informational notices the service published for this model, such as an upcoming change or a recommended alternative. Present only when the service published at least one notice. Hosts should surface these without implying anything is wrong with the model.
+ [JsonPropertyName("infoMessages")]
+ public IList? InfoMessages { get; set; }
+
/// Model capability category for grouping in the model picker.
[JsonPropertyName("modelPickerCategory")]
public ModelPickerCategory? ModelPickerCategory { get; set; }
@@ -338,6 +389,14 @@ public sealed class Model
/// Supported reasoning effort levels (only present if model supports reasoning effort).
[JsonPropertyName("supportedReasoningEfforts")]
public IList? SupportedReasoningEfforts { 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; }
+
+ /// Warning text the service requires hosts to surface for this model. Present only when the service published at least one warning.
+ [JsonPropertyName("warningText")]
+ public ModelWarningText? WarningText { get; set; }
}
/// List of Copilot models available to the resolved user, including capabilities and billing metadata.
@@ -491,6 +550,7 @@ internal sealed class AccountGetQuotaRequest
[JsonDerivedType(typeof(AuthInfoHmac), "hmac")]
[JsonDerivedType(typeof(AuthInfoEnv), "env")]
[JsonDerivedType(typeof(AuthInfoToken), "token")]
+[JsonDerivedType(typeof(AuthInfoTokenProvider), "token-provider")]
[JsonDerivedType(typeof(AuthInfoCopilotApiToken), "copilot-api-token")]
[JsonDerivedType(typeof(AuthInfoUser), "user")]
[JsonDerivedType(typeof(AuthInfoGhCli), "gh-cli")]
@@ -898,11 +958,39 @@ public partial class AuthInfoToken : AuthInfo
[JsonPropertyName("host")]
public required string Host { get; set; }
+ /// Opaque native GitHub credential registration backing this token identity, when applicable.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("registrationId")]
+ public string? RegistrationId { get; set; }
+
/// The token value itself. Treat as a secret.
[JsonPropertyName("token")]
public required string Token { get; set; }
}
+/// Authentication-info variant backed by an SDK GitHub token callback. It carries routing metadata but never a plaintext token.
+/// The token-provider variant of .
+[Experimental(Diagnostics.Experimental)]
+public partial class AuthInfoTokenProvider : AuthInfo
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "token-provider";
+
+ /// Snapshot of the authenticated user's Copilot subscription info, if known.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("copilotUser")]
+ public CopilotUserResponse? CopilotUser { get; set; }
+
+ /// Authentication host.
+ [JsonPropertyName("host")]
+ public required string Host { get; set; }
+
+ /// Opaque SDK callback registration identifier.
+ [JsonPropertyName("registrationId")]
+ public required string RegistrationId { get; set; }
+}
+
/// Authentication-info variant for direct Copilot API token auth sourced from environment variables, with public GitHub host.
/// The copilot-api-token variant of .
[Experimental(Diagnostics.Experimental)]
@@ -2695,6 +2783,10 @@ public sealed class InstalledPluginInfo
[JsonPropertyName("enabled")]
public bool Enabled { get; set; }
+ /// Absolute path of the marketplace directory a live plugin was resolved from. Present only on live, never-persisted records — a plugin belonging to a directory/local marketplace, which is loaded from its real directory on every pass instead of a copy under the installed-plugins cache. Its presence is what marks a listed plugin as live: such a plugin is always present on disk, so `enabled` is its only meaningful state and it is never "not installed".
+ [JsonPropertyName("installedFrom")]
+ public string? InstalledFrom { get; set; }
+
/// Marketplace the plugin came from. Empty string ("") for direct repo / URL / local installs.
[JsonPropertyName("marketplace")]
public string Marketplace { get; set; } = string.Empty;
@@ -4462,6 +4554,10 @@ public sealed class InstalledPlugin
[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;
@@ -5350,13 +5446,204 @@ public sealed class SessionSetCredentialsResult
public bool Success { get; set; }
}
+/// Authentication credentials accepted by session.gitHubAuth.setCredentials. Session-owned token-provider identities cannot be installed through this method.
+/// Polymorphic base type discriminated by type.
+[Experimental(Diagnostics.Experimental)]
+[JsonPolymorphic(
+ TypeDiscriminatorPropertyName = "type",
+ UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
+[JsonDerivedType(typeof(SettableAuthInfoHmac), "hmac")]
+[JsonDerivedType(typeof(SettableAuthInfoEnv), "env")]
+[JsonDerivedType(typeof(SettableAuthInfoToken), "token")]
+[JsonDerivedType(typeof(SettableAuthInfoCopilotApiToken), "copilot-api-token")]
+[JsonDerivedType(typeof(SettableAuthInfoUser), "user")]
+[JsonDerivedType(typeof(SettableAuthInfoGhCli), "gh-cli")]
+[JsonDerivedType(typeof(SettableAuthInfoApiKey), "api-key")]
+public partial class SettableAuthInfo
+{
+ /// The type discriminator.
+ [JsonPropertyName("type")]
+ public virtual string Type { get; set; } = string.Empty;
+}
+
+
+/// Authentication-info input variant for GitHub-internal HMAC auth, carrying the public GitHub host and HMAC secret.
+/// The hmac variant of .
+[Experimental(Diagnostics.Experimental)]
+public partial class SettableAuthInfoHmac : SettableAuthInfo
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "hmac";
+
+ /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("copilotUser")]
+ public CopilotUserResponse? CopilotUser { get; set; }
+
+ /// HMAC secret used to sign requests.
+ [JsonPropertyName("hmac")]
+ public required string Hmac { get; set; }
+
+ /// Authentication host. HMAC auth always targets the public GitHub host.
+ [JsonPropertyName("host")]
+ public required string Host { get; set; }
+}
+
+/// Authentication-info input variant for a token sourced from an environment variable, with host, optional login, token, and env var name.
+/// The env variant of .
+[Experimental(Diagnostics.Experimental)]
+public partial class SettableAuthInfoEnv : SettableAuthInfo
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "env";
+
+ /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("copilotUser")]
+ public CopilotUserResponse? CopilotUser { get; set; }
+
+ /// Name of the environment variable the token was sourced from.
+ [JsonPropertyName("envVar")]
+ public required string EnvVar { get; set; }
+
+ /// Authentication host (e.g. https://github.com or a GHES host).
+ [JsonPropertyName("host")]
+ public required string Host { get; set; }
+
+ /// User login associated with the token. Undefined for server-to-server tokens (those starting with `ghs_`).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("login")]
+ public string? Login { get; set; }
+
+ /// The token value itself. Treat as a secret.
+ [JsonPropertyName("token")]
+ public required string Token { get; set; }
+}
+
+/// Token authentication accepted by session.gitHubAuth.setCredentials.
+/// The token variant of .
+[Experimental(Diagnostics.Experimental)]
+public partial class SettableAuthInfoToken : SettableAuthInfo
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "token";
+
+ /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("copilotUser")]
+ public CopilotUserResponse? CopilotUser { get; set; }
+
+ /// Authentication host.
+ [JsonPropertyName("host")]
+ public required string Host { get; set; }
+
+ /// The token value itself. Treat as a secret.
+ [JsonPropertyName("token")]
+ public required string Token { get; set; }
+}
+
+/// Authentication-info variant for direct Copilot API token auth sourced from environment variables, with public GitHub host.
+/// The copilot-api-token variant of .
+[Experimental(Diagnostics.Experimental)]
+public partial class SettableAuthInfoCopilotApiToken : SettableAuthInfo
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "copilot-api-token";
+
+ /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("copilotUser")]
+ public CopilotUserResponse? CopilotUser { get; set; }
+
+ /// Authentication host (always the public GitHub host).
+ [JsonPropertyName("host")]
+ public required string Host { get; set; }
+}
+
+/// Authentication-info variant for OAuth user auth, with host and login; the token remains in the runtime secret store.
+/// The user variant of .
+[Experimental(Diagnostics.Experimental)]
+public partial class SettableAuthInfoUser : SettableAuthInfo
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "user";
+
+ /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("copilotUser")]
+ public CopilotUserResponse? CopilotUser { get; set; }
+
+ /// Authentication host.
+ [JsonPropertyName("host")]
+ public required string Host { get; set; }
+
+ /// OAuth user login.
+ [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(Diagnostics.Experimental)]
+public partial class SettableAuthInfoGhCli : SettableAuthInfo
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "gh-cli";
+
+ /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("copilotUser")]
+ public CopilotUserResponse? CopilotUser { get; set; }
+
+ /// Authentication host.
+ [JsonPropertyName("host")]
+ public required string Host { get; set; }
+
+ /// User login as reported by `gh auth status`.
+ [JsonPropertyName("login")]
+ public required string Login { get; set; }
+
+ /// The token returned by `gh auth token`. Treat as a secret.
+ [JsonPropertyName("token")]
+ public required string Token { get; set; }
+}
+
+/// Authentication-info input variant for API-key authentication to a non-GitHub LLM provider, carrying the secret `apiKey` and host.
+/// The api-key variant of .
+[Experimental(Diagnostics.Experimental)]
+public partial class SettableAuthInfoApiKey : SettableAuthInfo
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "api-key";
+
+ /// The API key. Treat as a secret.
+ [JsonPropertyName("apiKey")]
+ public required string ApiKey { get; set; }
+
+ /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("copilotUser")]
+ public CopilotUserResponse? CopilotUser { get; set; }
+
+ /// Authentication host.
+ [JsonPropertyName("host")]
+ public required string Host { get; set; }
+}
+
/// New auth credentials to install on the session. Omit to leave credentials unchanged.
[Experimental(Diagnostics.Experimental)]
internal sealed class SessionSetCredentialsParams
{
/// The new auth credentials to install on the session. When omitted or `undefined`, the call is a no-op and the session's existing credentials are preserved. The runtime installs the supplied value immediately for outbound model/API requests. When the credential carries a raw token (`token`, `env`, or `gh-cli`) but no `copilotUser`, the runtime additionally re-resolves `copilotUser` server-side (best-effort, asynchronously, after the synchronous install) so plan/quota/billing metadata regains fidelity; on resolution failure the verbatim credential remains installed. It does NOT otherwise validate the credential. Several variants carry secret material; treat this method's params as containing secrets at rest and in transit.
[JsonPropertyName("credentials")]
- public AuthInfo? Credentials { get; set; }
+ public SettableAuthInfo? Credentials { get; set; }
/// Target session identifier.
[JsonPropertyName("sessionId")]
@@ -5383,6 +5670,10 @@ public sealed class AuthIdentity
[JsonPropertyName("login")]
public string? Login { get; set; }
+ /// Opaque SDK GitHub credential registration backing this identity. Routing metadata only; never a credential.
+ [JsonPropertyName("registrationId")]
+ public string? RegistrationId { get; set; }
+
/// Authentication type.
[JsonPropertyName("type")]
public AuthInfoType Type { get; set; }
@@ -9384,7 +9675,7 @@ public partial class McpOauthPendingRequestResponseToken : McpOauthPendingReques
[JsonPropertyName("expiresIn")]
public long? ExpiresIn { get; set; }
- /// OAuth token type. Defaults to Bearer when omitted.
+ /// OAuth token type. Defaults to bearer when omitted.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("tokenType")]
public string? TokenType { get; set; }
@@ -10588,6 +10879,10 @@ public sealed class SessionInstalledPlugin
[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;
@@ -10802,7 +11097,7 @@ public sealed class SandboxConfig
[JsonPropertyName("addCurrentWorkingDirectory")]
public bool? AddCurrentWorkingDirectory { get; set; }
- /// Whether to auto-grant read access to the tool directories discovered on PATH and in toolchain environment variables (GOROOT, CARGO_HOME, JAVA_HOME, VIRTUAL_ENV, and similar), and to common developer-tool caches, registries, and toolchains in their default home locations (cargo, go, npm, Maven, and more), plus read-write access to (and, on Unix, up-front creation of) the scratch caches builds write on every run (go-build, ccache, sccache, Gradle caches, Cargo lock/tracker files), so builds work without extra configuration; a relocated CARGO_HOME additionally gets its Cargo lock files granted read-write. Set to false to disable every grant listed above: user-installed toolchains (rustup, nvm, pyenv, conda, pipx) then need explicit userPolicy.filesystem entries — readonlyPaths to read them, plus readwriteFiles for a relocated CARGO_HOME's .package-cache and .global-cache, which Cargo locks on every build. Only these developer-tool grants are affected: the working directory (see addCurrentWorkingDirectory), temporary storage, session log paths, and system locations follow their own rules and stay granted, so commands still run. Default: true (enabled by default; set to false to opt out).
+ /// Whether to auto-grant read access to the tool directories discovered on PATH and in toolchain environment variables (GOROOT, CARGO_HOME, JAVA_HOME, VIRTUAL_ENV, and similar), and to common developer-tool caches, registries, and toolchains in their default home locations (cargo, go, npm, Maven, and more), plus read-write access to (and up-front creation of) the scratch caches builds write on every run (go-build, ccache, sccache, Gradle caches, Cargo lock/tracker files), so builds work without extra configuration; a relocated CARGO_HOME additionally gets its Cargo lock files granted read-write. Set to false to disable every grant listed above: user-installed toolchains (rustup, nvm, pyenv, conda, pipx) then need explicit userPolicy.filesystem entries — readonlyPaths to read them, plus readwriteFiles for a relocated CARGO_HOME's .package-cache and .global-cache, which Cargo locks on every build. Only these developer-tool grants are affected: the working directory (see addCurrentWorkingDirectory), temporary storage, session log paths, and system locations follow their own rules and stay granted, so commands still run. Default: true (enabled by default; set to false to opt out).
[JsonPropertyName("allowDevToolAccess")]
public bool? AllowDevToolAccess { get; set; }
@@ -11035,6 +11330,10 @@ internal sealed class SessionUpdateOptionsParams
[JsonPropertyName("includedBuiltinAgents")]
public IList? IncludedBuiltinAgents { get; set; }
+ /// Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available. Set to null to remove the allowlist restriction.
+ [JsonPropertyName("includedBuiltinSkills")]
+ public IList? IncludedBuiltinSkills { get; set; }
+
/// Full set of installed plugins for the session. Replaces the existing list; the runtime invalidates the skills cache only when the list materially changes.
[JsonPropertyName("installedPlugins")]
public IList? InstalledPlugins { get; set; }
@@ -11095,6 +11394,11 @@ internal sealed class SessionUpdateOptionsParams
[JsonPropertyName("sandboxConfig")]
public SandboxConfig? SandboxConfig { get; set; }
+ /// Origin of the sandbox choice. The runtime uses this only for internal telemetry provenance; managed policy is derived independently.
+ [JsonInclude]
+ [JsonPropertyName("sandboxConfigSource")]
+ internal SandboxConfigSource? SandboxConfigSource { get; set; }
+
/// Replaces the session's capability set with the given list. Use to enable or disable capabilities mid-session (e.g., remove `memory` for reproducible scripted runs). Omit the field to leave the existing capability set unchanged.
[JsonPropertyName("sessionCapabilities")]
public IList? SessionCapabilities { get; set; }
@@ -13485,7 +13789,7 @@ public sealed class PermissionsConfigureAdditionalContentExclusionPolicy
[Experimental(Diagnostics.Experimental)]
public sealed class PermissionPathsConfig
{
- /// Additional directories to allow tool access to (in addition to the session's working directory). When `unrestricted` is true, these are still pre-populated on the UnrestrictedPathManager so they remain visible via getDirectories() (e.g. for @-mention completion).
+ /// Additional directories to allow tool access to (in addition to the session's working directory). Conventional `.github/skills/` and `.github/agents/` definitions under them also join the session catalogs when their subsystem gates are enabled, so supplying a directory is a trust decision for configuration stored there. When `unrestricted` is true, these are still pre-populated on the UnrestrictedPathManager so they remain visible via getDirectories() (e.g. for @-mention completion).
[JsonPropertyName("additionalDirectories")]
public IList? AdditionalDirectories { get; set; }
@@ -13578,6 +13882,10 @@ public sealed class PermissionDecisionContext
[JsonPropertyName("outcome")]
public PermissionDecisionOutcome Outcome { get; set; }
+ /// Whether the responding client could ask a user interactively, was running headlessly, or had no response path. Omit when the client cannot determine this authoritatively.
+ [JsonPropertyName("responseCapability")]
+ public PermissionResponseCapability? ResponseCapability { get; set; }
+
/// Controlled reason or actor responsible for the response.
[JsonPropertyName("source")]
public PermissionDecisionSource Source { get; set; }
@@ -14466,7 +14774,7 @@ public sealed class PermissionsPathsAddResult
[Experimental(Diagnostics.Experimental)]
internal sealed class PermissionPathsAddParams
{
- /// Directory to add to the allow-list. The runtime resolves and validates the path before adding.
+ /// Directory to add to the allow-list. The runtime resolves and validates the path before adding, then loads conventional `.github/skills/` and `.github/agents/` definitions under it when their subsystem gates are enabled. Adding the directory is therefore also a trust decision for configuration stored there.
[JsonPropertyName("path")]
public string Path { get; set; } = string.Empty;
@@ -16233,6 +16541,10 @@ public sealed class QueuePendingItems
[Experimental(Diagnostics.Experimental)]
public sealed class QueuePendingItemsResult
{
+ /// How many leading entries of `steeringMessages` have already been folded into the running turn (and so have an emitted `user.message`), as opposed to still waiting for one. Absent for hosts that do not distinguish the two.
+ [JsonPropertyName("inFlightSteeringCount")]
+ public long? InFlightSteeringCount { get; set; }
+
/// Pending queued items in submission order. Includes user messages, queued slash commands, and queued model changes; omits internal system items.
[JsonPropertyName("items")]
public IList Items { get => field ??= []; set; }
@@ -18214,6 +18526,14 @@ public sealed class GitHubTelemetryClientInfo
[JsonPropertyName("copilot_plan")]
public string? CopilotPlan { get; set; }
+ /// Number of logical CPU cores on the host.
+ [JsonPropertyName("cpu_count")]
+ public long? CpuCount { get; set; }
+
+ /// Distinct CPU model names for the host, comma-separated.
+ [JsonPropertyName("cpu_model")]
+ public string? CpuModel { get; set; }
+
/// Stable machine identifier for the device.
[JsonPropertyName("dev_device_id")]
public string? DevDeviceId { get; set; }
@@ -18301,6 +18621,74 @@ public sealed class GitHubTelemetryNotification
public string? SessionId { get; set; }
}
+/// SDK host response to a GitHub credential request.
+/// Polymorphic base type discriminated by kind.
+[Experimental(Diagnostics.Experimental)]
+[JsonPolymorphic(
+ TypeDiscriminatorPropertyName = "kind",
+ UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
+[JsonDerivedType(typeof(GitHubTokenAcquireResultToken), "token")]
+[JsonDerivedType(typeof(GitHubTokenAcquireResultCancelled), "cancelled")]
+public partial class GitHubTokenAcquireResult
+{
+ /// The type discriminator.
+ [JsonPropertyName("kind")]
+ public virtual string Kind { get; set; } = string.Empty;
+}
+
+
+/// The token variant of .
+[Experimental(Diagnostics.Experimental)]
+public partial class GitHubTokenAcquireResultToken : GitHubTokenAcquireResult
+{
+ ///
+ [JsonIgnore]
+ public override string Kind => "token";
+
+ /// GitHub access token acquired by the SDK host.
+ [JsonPropertyName("accessToken")]
+ public required string AccessToken { get; set; }
+
+ /// Remaining token lifetime in seconds when callback execution completes. It must exceed the one-hour preflight refresh threshold.
+ [JsonPropertyName("expiresIn")]
+ public required long ExpiresIn { get; set; }
+
+ /// OAuth token type. Defaults to bearer when omitted.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("tokenType")]
+ public string? TokenType { get; set; }
+}
+
+/// The cancelled variant of .
+[Experimental(Diagnostics.Experimental)]
+public partial class GitHubTokenAcquireResultCancelled : GitHubTokenAcquireResult
+{
+ ///
+ [JsonIgnore]
+ public override string Kind => "cancelled";
+}
+
+/// Asks the SDK client to acquire a GitHub access token from an opaque callback registration.
+[Experimental(Diagnostics.Experimental)]
+public sealed class GitHubTokenAcquireRequest
+{
+ /// Effective GitHub host for which the callback must return a token.
+ [JsonPropertyName("host")]
+ public string Host { get; set; } = string.Empty;
+
+ /// Why the runtime is requesting a GitHub credential.
+ [JsonPropertyName("reason")]
+ public GitHubTokenAcquireReason Reason { get; set; }
+
+ /// Opaque identifier generated by the SDK for this callback registration.
+ [JsonPropertyName("registrationId")]
+ public string RegistrationId { get; set; } = string.Empty;
+
+ /// Session receiving the token. Absent only before a cloud session has been assigned its id.
+ [JsonPropertyName("sessionId")]
+ public string? SessionId { get; set; }
+}
+
/// Resolved Anthropic adaptive-thinking capability for a model.
[Experimental(Diagnostics.Experimental)]
[JsonConverter(typeof(Converter))]
@@ -22563,6 +22951,9 @@ public AuthInfoType(string value)
/// 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");
@@ -25168,6 +25559,84 @@ public override void Write(Utf8JsonWriter writer, OptionsUpdateReasoningSummary
}
+/// Origin of the sandbox choice supplied by an internal client.
+[Experimental(Diagnostics.Experimental)]
+[JsonConverter(typeof(Converter))]
+[DebuggerDisplay("{Value,nq}")]
+public readonly struct SandboxConfigSource : IEquatable
+{
+ private readonly string? _value;
+
+ /// Initializes a new instance of the struct.
+ /// The value to associate with this .
+ [JsonConstructor]
+ public SandboxConfigSource(string value)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(value);
+ _value = value;
+ }
+
+ /// Gets the value associated with this .
+ public string Value => _value ?? string.Empty;
+
+ /// The client applied the default because no sandbox preference was configured.
+ public static SandboxConfigSource NeverConfigured { get; } = new("never_configured");
+
+ /// The user's persisted settings enabled the sandbox.
+ public static SandboxConfigSource UserEnabled { get; } = new("user_enabled");
+
+ /// The user's persisted settings disabled the sandbox.
+ public static SandboxConfigSource UserDisabled { get; } = new("user_disabled");
+
+ /// A command-line flag selected the sandbox state for this session.
+ public static SandboxConfigSource SessionFlag { get; } = new("session_flag");
+
+ /// The user disabled the sandbox for the current session.
+ public static SandboxConfigSource SessionDisabled { get; } = new("session_disabled");
+
+ /// The client disabled the sandbox because the host cannot enforce it.
+ public static SandboxConfigSource UnsupportedHost { get; } = new("unsupported_host");
+
+ /// A repository policy selected the sandbox state.
+ public static SandboxConfigSource RepositoryPolicy { get; } = new("repository_policy");
+
+ /// Returns a value indicating whether two instances are equivalent.
+ public static bool operator ==(SandboxConfigSource left, SandboxConfigSource right) => left.Equals(right);
+
+ /// Returns a value indicating whether two instances are not equivalent.
+ public static bool operator !=(SandboxConfigSource left, SandboxConfigSource right) => !(left == right);
+
+ ///
+ public override bool Equals(object? obj) => obj is SandboxConfigSource other && Equals(other);
+
+ ///
+ public bool Equals(SandboxConfigSource 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 SandboxConfigSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert));
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, SandboxConfigSource value, JsonSerializerOptions options)
+ {
+ GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SandboxConfigSource));
+ }
+ }
+}
+
+
/// Session capability enabled for this session.
[Experimental(Diagnostics.Experimental)]
[JsonConverter(typeof(Converter))]
@@ -26680,6 +27149,72 @@ public override void Write(Utf8JsonWriter writer, PermissionDecisionOutcome valu
}
+/// Response capability available to the client when it settled a permission request.
+[Experimental(Diagnostics.Experimental)]
+[JsonConverter(typeof(Converter))]
+[DebuggerDisplay("{Value,nq}")]
+public readonly struct PermissionResponseCapability : IEquatable
+{
+ private readonly string? _value;
+
+ /// Initializes a new instance of the struct.
+ /// The value to associate with this .
+ [JsonConstructor]
+ public PermissionResponseCapability(string value)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(value);
+ _value = value;
+ }
+
+ /// Gets the value associated with this .
+ public string Value => _value ?? string.Empty;
+
+ /// The client could ask a user for this decision.
+ public static PermissionResponseCapability Interactive { get; } = new("interactive");
+
+ /// The client could return an automated response but could not ask a user.
+ public static PermissionResponseCapability Headless { get; } = new("headless");
+
+ /// The client had no response path available.
+ public static PermissionResponseCapability None { get; } = new("none");
+
+ /// Returns a value indicating whether two instances are equivalent.
+ public static bool operator ==(PermissionResponseCapability left, PermissionResponseCapability right) => left.Equals(right);
+
+ /// Returns a value indicating whether two instances are not equivalent.
+ public static bool operator !=(PermissionResponseCapability left, PermissionResponseCapability right) => !(left == right);
+
+ ///
+ public override bool Equals(object? obj) => obj is PermissionResponseCapability other && Equals(other);
+
+ ///
+ public bool Equals(PermissionResponseCapability 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 PermissionResponseCapability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert));
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, PermissionResponseCapability value, JsonSerializerOptions options)
+ {
+ GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionResponseCapability));
+ }
+ }
+}
+
+
/// Controlled reason or actor responsible for a permission response.
[Experimental(Diagnostics.Experimental)]
[JsonConverter(typeof(Converter))]
@@ -26778,6 +27313,9 @@ public PermissionDecisionSurface(string value)
/// The Copilot App client.
public static PermissionDecisionSurface CopilotApp { get; } = new("copilot_app");
+ /// An Agent Client Protocol host.
+ public static PermissionDecisionSurface Acp { get; } = new("acp");
+
/// A generic Copilot SDK client.
public static PermissionDecisionSurface Sdk { get; } = new("sdk");
@@ -28824,6 +29362,69 @@ public override void Write(Utf8JsonWriter writer, LlmInferenceHttpRequestStartTr
}
+/// Why the runtime is requesting a GitHub credential.
+[Experimental(Diagnostics.Experimental)]
+[JsonConverter(typeof(Converter))]
+[DebuggerDisplay("{Value,nq}")]
+public readonly struct GitHubTokenAcquireReason : IEquatable
+{
+ private readonly string? _value;
+
+ /// Initializes a new instance of the struct.
+ /// The value to associate with this .
+ [JsonConstructor]
+ public GitHubTokenAcquireReason(string value)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(value);
+ _value = value;
+ }
+
+ /// Gets the value associated with this .
+ public string Value => _value ?? string.Empty;
+
+ /// The runtime is acquiring the registration's first credential.
+ public static GitHubTokenAcquireReason Initial { get; } = new("initial");
+
+ /// The runtime is replacing a credential that is approaching expiry.
+ public static GitHubTokenAcquireReason Refresh { get; } = new("refresh");
+
+ /// Returns a value indicating whether two instances are equivalent.
+ public static bool operator ==(GitHubTokenAcquireReason left, GitHubTokenAcquireReason right) => left.Equals(right);
+
+ /// Returns a value indicating whether two instances are not equivalent.
+ public static bool operator !=(GitHubTokenAcquireReason left, GitHubTokenAcquireReason right) => !(left == right);
+
+ ///
+ public override bool Equals(object? obj) => obj is GitHubTokenAcquireReason other && Equals(other);
+
+ ///
+ public bool Equals(GitHubTokenAcquireReason 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 GitHubTokenAcquireReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert));
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, GitHubTokenAcquireReason value, JsonSerializerOptions options)
+ {
+ GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(GitHubTokenAcquireReason));
+ }
+ }
+}
+
+
/// Provides server-scoped RPC methods (no session required).
public sealed class ServerRpc
{
@@ -28847,13 +29448,14 @@ public async Task PingAsync(string? message = null, CancellationToke
/// Performs the SDK server connection handshake and validates the optional connection token. Marked internal because this is JSON-RPC transport plumbing invoked automatically by an SDK client's own `connect()` wrapper, not a user-facing method. Stays internal as long as the SDK client owns the handshake; would only become public if the SDK ever exposed the raw schema surface to consumers without a connection wrapper.
/// Opt this connection in to GitHub telemetry forwarding for its lifetime. When set, the runtime forwards every internal telemetry event it emits — across all sessions, plus sessionless events — to this connection over the `gitHubTelemetry.event` notification. Regular events are also written to the runtime's normal GitHub/CTS path (dual-write); host-only compatibility events are forward-only and intentionally skip that path. Intended for first-party hosts that re-emit the events into their own telemetry stores. Both unrestricted and restricted events are forwarded, each tagged with a `restricted` discriminator; a backstop drops restricted events when restricted telemetry is disabled — using the process-global gate for ordinary events and an explicit session-scoped decision for host-only events.
+ /// Identity of the integrating host. Optional; omit it to keep the default attribution.
/// Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN.
/// The to monitor for cancellation requests. The default is .
/// Handshake result reporting the server's protocol version and package version on success.
[Experimental(Diagnostics.Experimental)]
- internal async Task ConnectAsync(bool? enableGitHubTelemetryForwarding = null, string? token = null, CancellationToken cancellationToken = default)
+ internal async Task ConnectAsync(bool? enableGitHubTelemetryForwarding = null, ConnectClientInfo? clientInfo = null, string? token = null, CancellationToken cancellationToken = default)
{
- var request = new ConnectRequest { EnableGitHubTelemetryForwarding = enableGitHubTelemetryForwarding, Token = token };
+ var request = new ConnectRequest { EnableGitHubTelemetryForwarding = enableGitHubTelemetryForwarding, ClientInfo = clientInfo, Token = token };
return await CopilotClient.InvokeRpcAsync(_rpc, "connect", [request], cancellationToken);
}
@@ -30665,7 +31267,7 @@ public async Task GetStatusAsync(CancellationToken cancellati
/// The new auth credentials to install on the session. When omitted or `undefined`, the call is a no-op and the session's existing credentials are preserved. The runtime installs the supplied value immediately for outbound model/API requests. When the credential carries a raw token (`token`, `env`, or `gh-cli`) but no `copilotUser`, the runtime additionally re-resolves `copilotUser` server-side (best-effort, asynchronously, after the synchronous install) so plan/quota/billing metadata regains fidelity; on resolution failure the verbatim credential remains installed. It does NOT otherwise validate the credential. Several variants carry secret material; treat this method's params as containing secrets at rest and in transit.
/// The to monitor for cancellation requests. The default is .
/// Indicates whether the credential update succeeded.
- public async Task SetCredentialsAsync(AuthInfo? credentials = null, CancellationToken cancellationToken = default)
+ public async Task SetCredentialsAsync(SettableAuthInfo? credentials = null, CancellationToken cancellationToken = default)
{
_session.ThrowIfDisposed();
@@ -32631,10 +33233,12 @@ internal OptionsApi(CopilotSession session)
/// Use shell.initProfile instead. Shell init profile (`None` or `NonInteractive`).
/// PowerShell process flags applied to built-in and user-requested shell commands.
/// Resolved sandbox configuration.
+ /// Origin of the sandbox choice. The runtime uses this only for internal telemetry provenance; managed policy is derived independently.
/// Whether interactive shell sessions are logged.
/// How env values are passed to MCP servers (`direct` inlines literal values; `indirect` resolves at launch).
/// Whether to include instructions from every MCP server in the system prompt instead of only allowlisted servers.
/// Additional directories to search for skills.
+ /// Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available. Set to null to remove the allowlist restriction.
/// Skill IDs that should be excluded from this session.
/// Whether to discover custom instructions on demand after successful file views (AGENTS.md / CLAUDE.md / .github/copilot-instructions.md surfacing). Combined with `skipCustomInstructions`.
/// Maximum decoded byte size of a single model-facing binary tool result (e.g. an image) persisted inline in session events and re-presented to the model on later turns / resume. Larger results are persisted as a metadata-only marker and shown to the model as a short text note. Defaults to 10 MB.
@@ -32667,11 +33271,11 @@ internal OptionsApi(CopilotSession session)
/// Optional session limits. Pass null to clear the session limits.
/// The to monitor for cancellation requests. The default is .
/// Indicates whether the session options patch was applied successfully.
- public async Task UpdateAsync(string? model = null, ModelCapabilitiesOverride? modelCapabilitiesOverrides = null, string? reasoningEffort = null, OptionsUpdateReasoningSummary? reasoningSummary = null, Verbosity? verbosity = null, string? clientName = null, string? lspClientName = null, string? integrationId = null, IDictionary? featureFlags = null, bool? isExperimentalMode = null, ProviderConfig? provider = null, CapiSessionOptions? capi = null, string? workingDirectory = null, IList? availableTools = null, IList? excludedTools = null, IList? includedBuiltinAgents = null, IList? excludedBuiltinAgents = null, OptionsUpdateToolFilterPrecedence? toolFilterPrecedence = null, bool? enableScriptSafety = null, ShellOptions? shell = null, string? shellInitProfile = null, IList? shellProcessFlags = null, SandboxConfig? sandboxConfig = null, bool? logInteractiveShells = null, OptionsUpdateEnvValueMode? envValueMode = null, bool? allowAllMcpServerInstructions = null, IList? skillDirectories = null, IList? disabledSkills = null, bool? enableOnDemandInstructionDiscovery = null, long? maxInlineBinaryBytes = null, IList? installedPlugins = null, bool? customAgentsLocalOnly = null, bool? suppressCustomAgentPrompt = null, bool? skipCustomInstructions = null, IList? disabledInstructionSources = null, bool? coauthorEnabled = null, string? trajectoryFile = null, bool? enableStreaming = null, string? copilotUrl = null, bool? askUserDisabled = null, bool? continueOnAutoMode = null, bool? runningInInteractiveMode = null, bool? enableReasoningSummaries = null, string? agentContext = null, string? eventsLogDirectory = null, bool? eventsLogIncludesSubagents = null, IList? additionalContentExclusionPolicies = null, bool? manageScheduleEnabled = null, IList? sessionCapabilities = null, bool? skipEmbeddingRetrieval = null, string? organizationCustomInstructions = null, bool? enableFileHooks = null, bool? enableHostGitOperations = null, bool? enableSessionStore = null, bool? enableSkills = null, OptionsUpdateContextTier? contextTier = null, SessionLimitsConfig? sessionLimits = null, CancellationToken cancellationToken = default)
+ public async Task UpdateAsync(string? model = null, ModelCapabilitiesOverride? modelCapabilitiesOverrides = null, string? reasoningEffort = null, OptionsUpdateReasoningSummary? reasoningSummary = null, Verbosity? verbosity = null, string? clientName = null, string? lspClientName = null, string? integrationId = null, IDictionary? featureFlags = null, bool? isExperimentalMode = null, ProviderConfig? provider = null, CapiSessionOptions? capi = null, string? workingDirectory = null, IList? availableTools = null, IList? excludedTools = null, IList? includedBuiltinAgents = null, IList? excludedBuiltinAgents = null, OptionsUpdateToolFilterPrecedence? toolFilterPrecedence = null, bool? enableScriptSafety = null, ShellOptions? shell = null, string? shellInitProfile = null, IList? shellProcessFlags = null, SandboxConfig? sandboxConfig = null, SandboxConfigSource? sandboxConfigSource = null, bool? logInteractiveShells = null, OptionsUpdateEnvValueMode? envValueMode = null, bool? allowAllMcpServerInstructions = null, IList? skillDirectories = null, IList? includedBuiltinSkills = null, IList? disabledSkills = null, bool? enableOnDemandInstructionDiscovery = null, long? maxInlineBinaryBytes = null, IList? installedPlugins = null, bool? customAgentsLocalOnly = null, bool? suppressCustomAgentPrompt = null, bool? skipCustomInstructions = null, IList? disabledInstructionSources = null, bool? coauthorEnabled = null, string? trajectoryFile = null, bool? enableStreaming = null, string? copilotUrl = null, bool? askUserDisabled = null, bool? continueOnAutoMode = null, bool? runningInInteractiveMode = null, bool? enableReasoningSummaries = null, string? agentContext = null, string? eventsLogDirectory = null, bool? eventsLogIncludesSubagents = null, IList? additionalContentExclusionPolicies = null, bool? manageScheduleEnabled = null, IList? sessionCapabilities = null, bool? skipEmbeddingRetrieval = null, string? organizationCustomInstructions = null, bool? enableFileHooks = null, bool? enableHostGitOperations = null, bool? enableSessionStore = null, bool? enableSkills = null, OptionsUpdateContextTier? contextTier = null, SessionLimitsConfig? sessionLimits = null, CancellationToken cancellationToken = default)
{
_session.ThrowIfDisposed();
- var request = new SessionUpdateOptionsParams { SessionId = _session.SessionId, Model = model, ModelCapabilitiesOverrides = modelCapabilitiesOverrides, ReasoningEffort = reasoningEffort, ReasoningSummary = reasoningSummary, Verbosity = verbosity, ClientName = clientName, LspClientName = lspClientName, IntegrationId = integrationId, FeatureFlags = featureFlags, IsExperimentalMode = isExperimentalMode, Provider = provider, Capi = capi, WorkingDirectory = workingDirectory, AvailableTools = availableTools, ExcludedTools = excludedTools, IncludedBuiltinAgents = includedBuiltinAgents, ExcludedBuiltinAgents = excludedBuiltinAgents, ToolFilterPrecedence = toolFilterPrecedence, EnableScriptSafety = enableScriptSafety, Shell = shell, ShellInitProfile = shellInitProfile, ShellProcessFlags = shellProcessFlags, SandboxConfig = sandboxConfig, LogInteractiveShells = logInteractiveShells, EnvValueMode = envValueMode, AllowAllMcpServerInstructions = allowAllMcpServerInstructions, SkillDirectories = skillDirectories, DisabledSkills = disabledSkills, EnableOnDemandInstructionDiscovery = enableOnDemandInstructionDiscovery, MaxInlineBinaryBytes = maxInlineBinaryBytes, InstalledPlugins = installedPlugins, CustomAgentsLocalOnly = customAgentsLocalOnly, SuppressCustomAgentPrompt = suppressCustomAgentPrompt, SkipCustomInstructions = skipCustomInstructions, DisabledInstructionSources = disabledInstructionSources, CoauthorEnabled = coauthorEnabled, TrajectoryFile = trajectoryFile, EnableStreaming = enableStreaming, CopilotUrl = copilotUrl, AskUserDisabled = askUserDisabled, ContinueOnAutoMode = continueOnAutoMode, RunningInInteractiveMode = runningInInteractiveMode, EnableReasoningSummaries = enableReasoningSummaries, AgentContext = agentContext, EventsLogDirectory = eventsLogDirectory, EventsLogIncludesSubagents = eventsLogIncludesSubagents, AdditionalContentExclusionPolicies = additionalContentExclusionPolicies, ManageScheduleEnabled = manageScheduleEnabled, SessionCapabilities = sessionCapabilities, SkipEmbeddingRetrieval = skipEmbeddingRetrieval, OrganizationCustomInstructions = organizationCustomInstructions, EnableFileHooks = enableFileHooks, EnableHostGitOperations = enableHostGitOperations, EnableSessionStore = enableSessionStore, EnableSkills = enableSkills, ContextTier = contextTier, SessionLimits = sessionLimits };
+ var request = new SessionUpdateOptionsParams { SessionId = _session.SessionId, Model = model, ModelCapabilitiesOverrides = modelCapabilitiesOverrides, ReasoningEffort = reasoningEffort, ReasoningSummary = reasoningSummary, Verbosity = verbosity, ClientName = clientName, LspClientName = lspClientName, IntegrationId = integrationId, FeatureFlags = featureFlags, IsExperimentalMode = isExperimentalMode, Provider = provider, Capi = capi, WorkingDirectory = workingDirectory, AvailableTools = availableTools, ExcludedTools = excludedTools, IncludedBuiltinAgents = includedBuiltinAgents, ExcludedBuiltinAgents = excludedBuiltinAgents, ToolFilterPrecedence = toolFilterPrecedence, EnableScriptSafety = enableScriptSafety, Shell = shell, ShellInitProfile = shellInitProfile, ShellProcessFlags = shellProcessFlags, SandboxConfig = sandboxConfig, SandboxConfigSource = sandboxConfigSource, LogInteractiveShells = logInteractiveShells, EnvValueMode = envValueMode, AllowAllMcpServerInstructions = allowAllMcpServerInstructions, SkillDirectories = skillDirectories, IncludedBuiltinSkills = includedBuiltinSkills, DisabledSkills = disabledSkills, EnableOnDemandInstructionDiscovery = enableOnDemandInstructionDiscovery, MaxInlineBinaryBytes = maxInlineBinaryBytes, InstalledPlugins = installedPlugins, CustomAgentsLocalOnly = customAgentsLocalOnly, SuppressCustomAgentPrompt = suppressCustomAgentPrompt, SkipCustomInstructions = skipCustomInstructions, DisabledInstructionSources = disabledInstructionSources, CoauthorEnabled = coauthorEnabled, TrajectoryFile = trajectoryFile, EnableStreaming = enableStreaming, CopilotUrl = copilotUrl, AskUserDisabled = askUserDisabled, ContinueOnAutoMode = continueOnAutoMode, RunningInInteractiveMode = runningInInteractiveMode, EnableReasoningSummaries = enableReasoningSummaries, AgentContext = agentContext, EventsLogDirectory = eventsLogDirectory, EventsLogIncludesSubagents = eventsLogIncludesSubagents, AdditionalContentExclusionPolicies = additionalContentExclusionPolicies, ManageScheduleEnabled = manageScheduleEnabled, SessionCapabilities = sessionCapabilities, SkipEmbeddingRetrieval = skipEmbeddingRetrieval, OrganizationCustomInstructions = organizationCustomInstructions, EnableFileHooks = enableFileHooks, EnableHostGitOperations = enableHostGitOperations, EnableSessionStore = enableSessionStore, EnableSkills = enableSkills, ContextTier = contextTier, SessionLimits = sessionLimits };
return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.options.update", [request], cancellationToken);
}
}
@@ -33388,8 +33992,8 @@ public async Task ListAsync(CancellationToken cancellationT
return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.permissions.paths.list", [request], cancellationToken);
}
- /// Adds a directory to the session's allow-list.
- /// Directory to add to the allow-list. The runtime resolves and validates the path before adding.
+ /// Adds a directory to the session's allow-list and activates conventional skill and agent definitions under it.
+ /// Directory to add to the allow-list. The runtime resolves and validates the path before adding, then loads conventional `.github/skills/` and `.github/agents/` definitions under it when their subsystem gates are enabled. Adding the directory is therefore also a trust decision for configuration stored there.
/// The to monitor for cancellation requests. The default is .
/// Indicates whether the operation succeeded.
public async Task AddAsync(string path, CancellationToken cancellationToken = default)
@@ -34786,6 +35390,17 @@ public interface IGitHubTelemetryHandler
Task EventAsync(GitHubTelemetryNotification request, CancellationToken cancellationToken = default);
}
+/// Handles `gitHubToken` client global API methods.
+[Experimental(Diagnostics.Experimental)]
+public interface IGitHubTokenHandler
+{
+ /// Asks the SDK client to mint a GitHub access token for a session whose configuration supplied a GitHub token provider. The runtime acquires the initial token during bootstrap and refreshes it during expiry preflight when one hour or less remains.
+ /// Asks the SDK client to acquire a GitHub access token from an opaque callback registration.
+ /// The to monitor for cancellation requests. The default is .
+ /// SDK host response to a GitHub credential request.
+ Task GetTokenAsync(GitHubTokenAcquireRequest request, CancellationToken cancellationToken = default);
+}
+
/// Provides all client global API handler groups for a connection.
public sealed class ClientGlobalApiHandlers
{
@@ -34797,6 +35412,9 @@ public sealed class ClientGlobalApiHandlers
/// Optional handler for GitHubTelemetry client global API methods.
public IGitHubTelemetryHandler? GitHubTelemetry { get; set; }
+
+ /// Optional handler for GitHubToken client global API methods.
+ public IGitHubTokenHandler? GitHubToken { get; set; }
}
/// Registers client global API handlers on a JSON-RPC connection.
@@ -34830,6 +35448,11 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
var handler = handlers.GitHubTelemetry ?? throw new InvalidOperationException("No gitHubTelemetry client-global handler registered");
await handler.EventAsync(request, cancellationToken);
}), singleObjectParam: true);
+ rpc.SetLocalRpcMethod("gitHubToken.getToken", (Func>)(async (request, cancellationToken) =>
+ {
+ var handler = handlers.GitHubToken ?? throw new InvalidOperationException("No gitHubToken client-global handler registered");
+ return await handler.GetTokenAsync(request, cancellationToken);
+ }), singleObjectParam: true);
}
}
@@ -34857,6 +35480,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(GitHub.Copilot.AssistantMessageDeltaData), TypeInfoPropertyName = "SessionEventsAssistantMessageDeltaData")]
[JsonSerializable(typeof(GitHub.Copilot.AssistantMessageDeltaEvent), TypeInfoPropertyName = "SessionEventsAssistantMessageDeltaEvent")]
[JsonSerializable(typeof(GitHub.Copilot.AssistantMessageEvent), TypeInfoPropertyName = "SessionEventsAssistantMessageEvent")]
+[JsonSerializable(typeof(GitHub.Copilot.AssistantMessageReasoningBlocks), TypeInfoPropertyName = "SessionEventsAssistantMessageReasoningBlocks")]
[JsonSerializable(typeof(GitHub.Copilot.AssistantMessageServerTools), TypeInfoPropertyName = "SessionEventsAssistantMessageServerTools")]
[JsonSerializable(typeof(GitHub.Copilot.AssistantMessageStartData), TypeInfoPropertyName = "SessionEventsAssistantMessageStartData")]
[JsonSerializable(typeof(GitHub.Copilot.AssistantMessageStartEvent), TypeInfoPropertyName = "SessionEventsAssistantMessageStartEvent")]
@@ -35028,6 +35652,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(GitHub.Copilot.ModelCallFailureRequestFingerprint), TypeInfoPropertyName = "SessionEventsModelCallFailureRequestFingerprint")]
[JsonSerializable(typeof(GitHub.Copilot.ModelCallFailureSource), TypeInfoPropertyName = "SessionEventsModelCallFailureSource")]
[JsonSerializable(typeof(GitHub.Copilot.ModelCallFailureTransport), TypeInfoPropertyName = "SessionEventsModelCallFailureTransport")]
+[JsonSerializable(typeof(GitHub.Copilot.ModelCallFinishedData), TypeInfoPropertyName = "SessionEventsModelCallFinishedData")]
+[JsonSerializable(typeof(GitHub.Copilot.ModelCallFinishedEvent), TypeInfoPropertyName = "SessionEventsModelCallFinishedEvent")]
+[JsonSerializable(typeof(GitHub.Copilot.ModelCallFinishedOutcome), TypeInfoPropertyName = "SessionEventsModelCallFinishedOutcome")]
[JsonSerializable(typeof(GitHub.Copilot.ModelCallStartData), TypeInfoPropertyName = "SessionEventsModelCallStartData")]
[JsonSerializable(typeof(GitHub.Copilot.ModelCallStartEvent), TypeInfoPropertyName = "SessionEventsModelCallStartEvent")]
[JsonSerializable(typeof(GitHub.Copilot.ModelChangeSource), TypeInfoPropertyName = "SessionEventsModelChangeSource")]
@@ -35283,6 +35910,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(CompletionsRequestRequest))]
[JsonSerializable(typeof(CompletionsRequestResult))]
[JsonSerializable(typeof(ConfigureSessionExtensionsParams))]
+[JsonSerializable(typeof(ConnectClientInfo))]
[JsonSerializable(typeof(ConnectRemoteSessionParams))]
[JsonSerializable(typeof(ConnectRequest))]
[JsonSerializable(typeof(ConnectResult))]
@@ -35374,6 +36002,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(GitHubTelemetryClientInfo))]
[JsonSerializable(typeof(GitHubTelemetryEvent))]
[JsonSerializable(typeof(GitHubTelemetryNotification))]
+[JsonSerializable(typeof(GitHubTokenAcquireRequest))]
+[JsonSerializable(typeof(GitHubTokenAcquireResult))]
[JsonSerializable(typeof(HandlePendingToolCallRequest))]
[JsonSerializable(typeof(HandlePendingToolCallResult))]
[JsonSerializable(typeof(HistoryAbortManualCompactionResult))]
@@ -35560,6 +36190,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(ModelCapabilitiesOverrideSupports))]
[JsonSerializable(typeof(ModelCapabilitiesSupports))]
[JsonSerializable(typeof(ModelList))]
+[JsonSerializable(typeof(ModelMessage))]
[JsonSerializable(typeof(ModelPickerPersistenceRequest))]
[JsonSerializable(typeof(ModelPickerSettingsContext))]
[JsonSerializable(typeof(ModelPickerSettingsContextEnvironment))]
@@ -35569,6 +36200,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(ModelSwitchConfirmation))]
[JsonSerializable(typeof(ModelSwitchToRequest))]
[JsonSerializable(typeof(ModelSwitchToResult))]
+[JsonSerializable(typeof(ModelWarningText))]
[JsonSerializable(typeof(ModelsListRequest))]
[JsonSerializable(typeof(MoveMcpLoadingToBackgroundResult))]
[JsonSerializable(typeof(NameGetResult))]
@@ -35949,6 +36581,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(SessionsStartRemoteControlRequest))]
[JsonSerializable(typeof(SessionsStopRemoteControlRequest))]
[JsonSerializable(typeof(SessionsTransferRemoteControlRequest))]
+[JsonSerializable(typeof(SettableAuthInfo))]
[JsonSerializable(typeof(ShellCancelUserRequestedRequest))]
[JsonSerializable(typeof(ShellCredentials))]
[JsonSerializable(typeof(ShellExecRequest))]
diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs
index fa0563a8e4..bb2f3f9979 100644
--- a/dotnet/src/Generated/SessionEvents.cs
+++ b/dotnet/src/Generated/SessionEvents.cs
@@ -68,6 +68,7 @@ namespace GitHub.Copilot;
[JsonDerivedType(typeof(McpResourcesListChangedEvent), "mcp.resources.list_changed")]
[JsonDerivedType(typeof(McpToolsListChangedEvent), "mcp.tools.list_changed")]
[JsonDerivedType(typeof(ModelCallFailureEvent), "model.call_failure")]
+[JsonDerivedType(typeof(ModelCallFinishedEvent), "model.call_finished")]
[JsonDerivedType(typeof(ModelCallStartEvent), "model.call_start")]
[JsonDerivedType(typeof(PendingMessagesModifiedEvent), "pending_messages.modified")]
[JsonDerivedType(typeof(PermissionCompletedEvent), "permission.completed")]
@@ -825,6 +826,19 @@ public sealed partial class ModelCallFailureEvent : SessionEvent
public required ModelCallFailureData Data { get; set; }
}
+/// Final lifecycle outcome for one logical model dispatch. A logical dispatch may include internal reconnect or fallback work, so event count is not provider HTTP-request count.
+/// Represents the model.call_finished event.
+public sealed partial class ModelCallFinishedEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "model.call_finished";
+
+ /// The model.call_finished event payload.
+ [JsonPropertyName("data")]
+ public required ModelCallFinishedData Data { get; set; }
+}
+
/// Model API dispatch metadata for internal telemetry.
/// Represents the model.call_start event.
public sealed partial class ModelCallStartEvent : SessionEvent
@@ -1988,6 +2002,11 @@ public sealed partial class SessionIdleData
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("aborted")]
public bool? Aborted { get; set; }
+
+ /// The session mode the agent was operating in when it went idle, when the mode is known. Lets turn-scoped consumers distinguish an autopilot continuation boundary (where the agent keeps working after this idle) from a genuine turn completion.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("mode")]
+ public SessionMode? Mode { get; set; }
}
/// Session title change payload containing the new display title.
@@ -3039,6 +3058,11 @@ public sealed partial class AssistantMessageData
[JsonPropertyName("phase")]
public string? Phase { get; set; }
+ /// Neutral provider-tagged reasoning content blocks preserved verbatim for round-tripping. `reasoningText` and `reasoningOpaque` are a lossy derived view of these blocks, retained for display.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("reasoningBlocks")]
+ public AssistantMessageReasoningBlocks? ReasoningBlocks { get; set; }
+
/// Opaque/encrypted extended thinking data from Anthropic models. Session-bound and stripped on resume.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("reasoningOpaque")]
@@ -3281,6 +3305,12 @@ public sealed partial class AssistantUsageData
[JsonPropertyName("outputTokens")]
public long? OutputTokens { get; set; }
+ /// Time to first observable model output in milliseconds. Includes text, reasoning, and tool-call output; only available for streaming requests that produce observable output.
+ [JsonConverter(typeof(MillisecondsTimeSpanConverter))]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("outputTtftMs")]
+ public TimeSpan? OutputTtft { get; set; }
+
/// Parent tool call ID when this usage originates from a sub-agent.
[EditorBrowsable(EditorBrowsableState.Never)]
#if NET5_0_OR_GREATER
@@ -3610,6 +3640,37 @@ public sealed partial class ModelCallFailureData
public ModelCallFailureTransport? Transport { get; set; }
}
+/// Final lifecycle outcome for one logical model dispatch. A logical dispatch may include internal reconnect or fallback work, so event count is not provider HTTP-request count.
+public sealed partial class ModelCallFinishedData
+{
+ /// Whether an accepted successful response requested the exact name and command semantics of a built-in file edit tool, including an external tool explicitly replacing that built-in name. Absent when the logical dispatch did not produce an accepted response.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("containsBuiltInFileEditRequest")]
+ public bool? ContainsBuiltInFileEditRequest { get; set; }
+
+ /// Monotonic elapsed time spent in the logical model dispatch, including any internal transport reconnect or fallback and excluding orchestrator retry backoff, tool execution, confirmations, and post-response processing.
+ [JsonConverter(typeof(MillisecondsTimeSpanConverter))]
+ [JsonPropertyName("dispatchDurationMs")]
+ public required TimeSpan DispatchDuration { get; set; }
+
+ /// Version of the built-in file-edit semantic classifier used for this event.
+ [JsonPropertyName("editClassifierVersion")]
+ public required long EditClassifierVersion { get; set; }
+
+ /// Identifier of the user interaction that owns the model dispatch, matching assistant.turn_start.interactionId when available.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("interactionId")]
+ public string? InteractionId { get; set; }
+
+ /// Final outcome after post-response acceptance processing.
+ [JsonPropertyName("outcome")]
+ public required ModelCallFinishedOutcome Outcome { get; set; }
+
+ /// Agent-loop iteration within the interaction that initiated the model dispatch.
+ [JsonPropertyName("turnId")]
+ public required string TurnId { get; set; }
+}
+
/// Model API dispatch metadata for internal telemetry.
public sealed partial class ModelCallStartData
{
@@ -3933,12 +3994,37 @@ public sealed partial class SubagentCompletedData
[JsonPropertyName("cancelled")]
public bool? Cancelled { get; set; }
+ /// Whether the first model actually dispatched matched the user's configured preference.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("configuredModelMatchesActual")]
+ public bool? ConfiguredModelMatchesActual { get; set; }
+
+ /// Concrete model the user configured for this sub-agent via `/subagents`, when present.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("configuredModelPreference")]
+ public string? ConfiguredModelPreference { get; set; }
+
/// Wall-clock duration of the sub-agent execution in milliseconds.
[JsonConverter(typeof(MillisecondsTimeSpanConverter))]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("durationMs")]
public TimeSpan? Duration { get; set; }
+ /// Whether the explicit task-call model matched the user's configured preference.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("explicitModelMatchesPreference")]
+ public bool? ExplicitModelMatchesPreference { get; set; }
+
+ /// Explicit model supplied by the parent agent on the task call, when present.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("explicitModelOverride")]
+ public string? ExplicitModelOverride { get; set; }
+
+ /// First model for which the sub-agent started an inference request, when one was dispatched.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("firstDispatchedModel")]
+ public string? FirstDispatchedModel { get; set; }
+
/// Model used by the sub-agent.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("model")]
@@ -3970,6 +4056,16 @@ public sealed partial class SubagentFailedData
[JsonPropertyName("agentName")]
public required string AgentName { get; set; }
+ /// Whether the first model actually dispatched matched the user's configured preference.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("configuredModelMatchesActual")]
+ public bool? ConfiguredModelMatchesActual { get; set; }
+
+ /// Concrete model the user configured for this sub-agent via `/subagents`, when present.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("configuredModelPreference")]
+ public string? ConfiguredModelPreference { get; set; }
+
/// Wall-clock duration of the sub-agent execution in milliseconds.
[JsonConverter(typeof(MillisecondsTimeSpanConverter))]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
@@ -3980,6 +4076,21 @@ public sealed partial class SubagentFailedData
[JsonPropertyName("error")]
public required string Error { get; set; }
+ /// Whether the explicit task-call model matched the user's configured preference.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("explicitModelMatchesPreference")]
+ public bool? ExplicitModelMatchesPreference { get; set; }
+
+ /// Explicit model supplied by the parent agent on the task call, when present.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("explicitModelOverride")]
+ public string? ExplicitModelOverride { get; set; }
+
+ /// First model for which the sub-agent started an inference request, when one was dispatched.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("firstDispatchedModel")]
+ public string? FirstDispatchedModel { get; set; }
+
/// Model selected for the sub-agent, when known.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("model")]
@@ -4721,6 +4832,11 @@ public sealed partial class SessionManagedSettingsResolvedData
[JsonPropertyName("permissionsAllowIntersected")]
public bool? PermissionsAllowIntersected { get; set; }
+ /// Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("sandboxEnabledByUndeterminedPolicy")]
+ public bool? SandboxEnabledByUndeterminedPolicy { get; set; }
+
/// Whether the server (account/org) managed-settings layer was present.
[JsonPropertyName("serverManaged")]
public required bool ServerManaged { get; set; }
@@ -6189,6 +6305,21 @@ public sealed partial class Citations
public required CitationSpan[] Spans { get; set; }
}
+/// Neutral provider-tagged reasoning content blocks preserved verbatim for round-tripping.
+/// Nested data type for AssistantMessageReasoningBlocks.
+[Experimental(Diagnostics.Experimental)]
+public sealed partial class AssistantMessageReasoningBlocks
+{
+ /// Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("blocks")]
+ public JsonElement[]? Blocks { get; set; }
+
+ /// Model provider that produced these reasoning blocks.
+ [JsonPropertyName("provider")]
+ public required string Provider { get; set; }
+}
+
/// Neutral provider-tagged server-side tool-use payload (tool search, advisor) for verbatim round-tripping.
/// Nested data type for AssistantMessageServerTools.
[Experimental(Diagnostics.Experimental)]
@@ -8288,6 +8419,11 @@ public sealed partial class PermissionPromptRequestMcp : PermissionPromptRequest
[JsonPropertyName("assistedApproval")]
public PermissionAssistedApproval? AssistedApproval { get; set; }
+ /// Whether the host may offer a server-wide "approve all tools from this server" blanket. Absent is treated as true; the runtime sends false when managed policy disables bypass-permissions mode, which forbids the server-wide escalation while still allowing per-tool approval.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("canOfferServerWideApproval")]
+ public bool? CanOfferServerWideApproval { get; set; }
+
/// Advisory runtime permission recommendation. The host remains responsible for deciding the request and may reject it.
[Experimental(Diagnostics.Experimental)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
@@ -9666,6 +9802,70 @@ public override void Write(Utf8JsonWriter writer, Verbosity value, JsonSerialize
}
}
+/// The session mode the agent is operating in.
+[JsonConverter(typeof(Converter))]
+[DebuggerDisplay("{Value,nq}")]
+public readonly struct SessionMode : IEquatable
+{
+ private readonly string? _value;
+
+ /// Initializes a new instance of the struct.
+ /// The value to associate with this .
+ [JsonConstructor]
+ public SessionMode(string value)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(value);
+ _value = value;
+ }
+
+ /// Gets the value associated with this .
+ public string Value => _value ?? string.Empty;
+
+ /// The agent is responding interactively to the user.
+ public static SessionMode Interactive { get; } = new("interactive");
+
+ /// The agent is preparing a plan before making changes.
+ public static SessionMode Plan { get; } = new("plan");
+
+ /// The agent is working autonomously toward task completion.
+ public static SessionMode Autopilot { get; } = new("autopilot");
+
+ /// Returns a value indicating whether two instances are equivalent.
+ public static bool operator ==(SessionMode left, SessionMode right) => left.Equals(right);
+
+ /// Returns a value indicating whether two instances are not equivalent.
+ public static bool operator !=(SessionMode left, SessionMode right) => !(left == right);
+
+ ///
+ public override bool Equals(object? obj) => obj is SessionMode other && Equals(other);
+
+ ///
+ public bool Equals(SessionMode 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 SessionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert));
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, SessionMode value, JsonSerializerOptions options)
+ {
+ GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionMode));
+ }
+ }
+}
+
/// Who created the schedule: `user` (an explicit user action such as `/every` or `/after`) or `model` (the agent via the `manage_schedule` tool). Gates whether a scheduled skill that opted out of model invocation may fire: only user-created schedules may.
[JsonConverter(typeof(Converter))]
[DebuggerDisplay("{Value,nq}")]
@@ -9946,70 +10146,6 @@ public override void Write(Utf8JsonWriter writer, ModelChangeSource value, JsonS
}
}
-/// The session mode the agent is operating in.
-[JsonConverter(typeof(Converter))]
-[DebuggerDisplay("{Value,nq}")]
-public readonly struct SessionMode : IEquatable
-{
- private readonly string? _value;
-
- /// Initializes a new instance of the struct.
- /// The value to associate with this .
- [JsonConstructor]
- public SessionMode(string value)
- {
- ArgumentException.ThrowIfNullOrWhiteSpace(value);
- _value = value;
- }
-
- /// Gets the value associated with this .
- public string Value => _value ?? string.Empty;
-
- /// The agent is responding interactively to the user.
- public static SessionMode Interactive { get; } = new("interactive");
-
- /// The agent is preparing a plan before making changes.
- public static SessionMode Plan { get; } = new("plan");
-
- /// The agent is working autonomously toward task completion.
- public static SessionMode Autopilot { get; } = new("autopilot");
-
- /// Returns a value indicating whether two instances are equivalent.
- public static bool operator ==(SessionMode left, SessionMode right) => left.Equals(right);
-
- /// Returns a value indicating whether two instances are not equivalent.
- public static bool operator !=(SessionMode left, SessionMode right) => !(left == right);
-
- ///
- public override bool Equals(object? obj) => obj is SessionMode other && Equals(other);
-
- ///
- public bool Equals(SessionMode 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 SessionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert));
- }
-
- ///
- public override void Write(Utf8JsonWriter writer, SessionMode value, JsonSerializerOptions options)
- {
- GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionMode));
- }
- }
-}
-
/// Permission mode for the session.
[Experimental(Diagnostics.Experimental)]
[JsonConverter(typeof(Converter))]
@@ -11341,6 +11477,73 @@ public override void Write(Utf8JsonWriter writer, ModelCallFailureSource value,
}
}
+/// Final outcome of one logical model dispatch after response acceptance processing.
+[JsonConverter(typeof(Converter))]
+[DebuggerDisplay("{Value,nq}")]
+public readonly struct ModelCallFinishedOutcome : IEquatable
+{
+ private readonly string? _value;
+
+ /// Initializes a new instance of the struct.
+ /// The value to associate with this .
+ [JsonConstructor]
+ public ModelCallFinishedOutcome(string value)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(value);
+ _value = value;
+ }
+
+ /// Gets the value associated with this .
+ public string Value => _value ?? string.Empty;
+
+ /// The provider response was accepted for continued agent processing.
+ public static ModelCallFinishedOutcome Success { get; } = new("success");
+
+ /// The dispatch ended with a provider or transport error.
+ public static ModelCallFinishedOutcome Error { get; } = new("error");
+
+ /// The dispatch was cancelled before an accepted response was produced.
+ public static ModelCallFinishedOutcome Cancelled { get; } = new("cancelled");
+
+ /// The provider response was rejected during post-response acceptance processing.
+ public static ModelCallFinishedOutcome Rejected { get; } = new("rejected");
+
+ /// Returns a value indicating whether two instances are equivalent.
+ public static bool operator ==(ModelCallFinishedOutcome left, ModelCallFinishedOutcome right) => left.Equals(right);
+
+ /// Returns a value indicating whether two instances are not equivalent.
+ public static bool operator !=(ModelCallFinishedOutcome left, ModelCallFinishedOutcome right) => !(left == right);
+
+ ///
+ public override bool Equals(object? obj) => obj is ModelCallFinishedOutcome other && Equals(other);
+
+ ///
+ public bool Equals(ModelCallFinishedOutcome 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 ModelCallFinishedOutcome Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert));
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, ModelCallFinishedOutcome value, JsonSerializerOptions options)
+ {
+ GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelCallFinishedOutcome));
+ }
+ }
+}
+
/// Finite reason code describing why the current turn was aborted.
[JsonConverter(typeof(Converter))]
[DebuggerDisplay("{Value,nq}")]
@@ -13403,6 +13606,9 @@ public ManagedSettingsEnforcedEscalation(string value)
/// Unrestricted URL fetch access.
public static ManagedSettingsEnforcedEscalation UnrestrictedUrls { get; } = new("unrestricted_urls");
+ /// A server-wide MCP "Always Allow" (or `--allow-tool <server>`) blanket that would auto-approve every tool from an MCP server. Capped to per-tool approval; each tool still prompts.
+ public static ManagedSettingsEnforcedEscalation ServerWideMcpApproval { get; } = new("server_wide_mcp_approval");
+
/// Returns a value indicating whether two instances are equivalent.
public static bool operator ==(ManagedSettingsEnforcedEscalation left, ManagedSettingsEnforcedEscalation right) => left.Equals(right);
@@ -14010,6 +14216,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu
[JsonSerializable(typeof(AssistantMessageDeltaData))]
[JsonSerializable(typeof(AssistantMessageDeltaEvent))]
[JsonSerializable(typeof(AssistantMessageEvent))]
+[JsonSerializable(typeof(AssistantMessageReasoningBlocks))]
[JsonSerializable(typeof(AssistantMessageServerTools))]
[JsonSerializable(typeof(AssistantMessageStartData))]
[JsonSerializable(typeof(AssistantMessageStartEvent))]
@@ -14149,6 +14356,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu
[JsonSerializable(typeof(ModelCallFailureData))]
[JsonSerializable(typeof(ModelCallFailureEvent))]
[JsonSerializable(typeof(ModelCallFailureRequestFingerprint))]
+[JsonSerializable(typeof(ModelCallFinishedData))]
+[JsonSerializable(typeof(ModelCallFinishedEvent))]
[JsonSerializable(typeof(ModelCallStartData))]
[JsonSerializable(typeof(ModelCallStartEvent))]
[JsonSerializable(typeof(OmittedBinaryResult))]
diff --git a/dotnet/src/GitHubTokenProvider.cs b/dotnet/src/GitHubTokenProvider.cs
new file mode 100644
index 0000000000..3a79d09662
--- /dev/null
+++ b/dotnet/src/GitHubTokenProvider.cs
@@ -0,0 +1,77 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace GitHub.Copilot;
+
+/// Why the runtime is requesting a GitHub token.
+[Experimental(Diagnostics.Experimental)]
+public enum GitHubTokenRequestReason
+{
+ /// The session needs its initial token.
+ Initial,
+
+ /// The session needs a refreshed token.
+ Refresh,
+}
+
+/// Arguments passed to a session-scoped GitHub token provider.
+[Experimental(Diagnostics.Experimental)]
+public sealed class GitHubTokenProviderArgs
+{
+ /// Gets the effective GitHub host for which a token is needed.
+ public required string Host { get; init; }
+
+ ///
+ /// Gets the session receiving the token, or before a
+ /// cloud session has been assigned an identifier.
+ ///
+ public string? SessionId { get; init; }
+
+ /// Gets whether the runtime needs an initial or refreshed token.
+ public required GitHubTokenRequestReason Reason { get; init; }
+}
+
+/// A GitHub access token returned by a session-scoped provider.
+[Experimental(Diagnostics.Experimental)]
+public sealed class GitHubToken
+{
+ /// Gets or sets the GitHub access token.
+ public required string AccessToken { get; set; }
+
+ /// Gets or sets the OAuth token type. The runtime defaults it to bearer.
+ public string? TokenType { get; set; }
+
+ ///
+ /// Gets or sets the required positive number of seconds remaining when the
+ /// callback completes. Production GitHub tokens typically last eight hours.
+ ///
+ public required long ExpiresIn { get; set; }
+
+ ///
+ public override string ToString()
+ => $"{nameof(GitHubToken)} {{ {nameof(TokenType)} = {TokenType}, {nameof(ExpiresIn)} = {ExpiresIn}, {nameof(AccessToken)} = }}";
+}
+
+/// The result returned by a session-scoped GitHub token provider.
+[Experimental(Diagnostics.Experimental)]
+public sealed class GitHubTokenProviderResult
+{
+ /// Gets whether token acquisition was cancelled.
+ public bool Cancelled { get; private init; }
+
+ /// Gets the acquired token, if acquisition was not cancelled.
+ public GitHubToken? Token { get; private init; }
+
+ /// Creates a successful token result.
+ public static GitHubTokenProviderResult FromToken(GitHubToken token)
+ {
+ ArgumentNullException.ThrowIfNull(token);
+ return new() { Token = token };
+ }
+
+ /// Creates a cancelled result.
+ public static GitHubTokenProviderResult Cancel() => new() { Cancelled = true };
+}
diff --git a/dotnet/src/JsonRpc.cs b/dotnet/src/JsonRpc.cs
index 36289d2e68..c5c444ea70 100644
--- a/dotnet/src/JsonRpc.cs
+++ b/dotnet/src/JsonRpc.cs
@@ -548,7 +548,11 @@ private async Task HandleIncomingMethodAsync(string methodName, JsonElement mess
if (requestId.HasValue)
{
- await SendResultResponseAsync(requestId.Value, result, cancellationToken).ConfigureAwait(false);
+ await SendResultResponseAsync(
+ requestId.Value,
+ result,
+ registration.ResultType,
+ cancellationToken).ConfigureAwait(false);
}
}
catch (Exception ex) when (ex is not OperationCanceledException)
@@ -772,18 +776,20 @@ private static bool TryGetPropertyCaseInsensitive(JsonElement obj, string name,
return doc.RootElement.Clone();
}
- private async Task SendResultResponseAsync(JsonElement id, object? result, CancellationToken cancellationToken)
+ private async Task SendResultResponseAsync(
+ JsonElement id,
+ object? result,
+ Type? declaredResultType,
+ CancellationToken cancellationToken)
{
try
{
- // Convert the result to a JsonElement using the runtime type, looked up via
- // the merged resolver. Source-gen serialization of an `object`-typed property
- // would otherwise have no way to find metadata for the actual response type
- // (e.g. SystemMessageTransformRpcResponse, SessionFsReadFileResult, ...).
+ // Prefer the handler's declared result type so polymorphic base types emit
+ // their discriminator. Fall back to the runtime type for untyped handlers.
JsonElement? resultElement = null;
if (result is not null)
{
- var typeInfo = _serializerOptions.GetTypeInfo(result.GetType());
+ var typeInfo = _serializerOptions.GetTypeInfo(declaredResultType ?? result.GetType());
resultElement = JsonSerializer.SerializeToElement(result, typeInfo);
}
@@ -863,6 +869,11 @@ public MethodRegistration(Delegate handler, bool singleObjectParam)
{
ValueTaskAsTaskMethod = GetMethodFromGenericMethodDefinition(returnType, s_valueTaskAsTask);
TaskResultGetter = GetMethodFromGenericMethodDefinition(ValueTaskAsTaskMethod.ReturnType, s_taskGetResult);
+ ResultType = returnType.GetGenericArguments()[0];
+ }
+ else if (returnType != typeof(void) && returnType != typeof(Task) && returnType != typeof(ValueTask))
+ {
+ ResultType = returnType;
}
}
@@ -871,6 +882,7 @@ public MethodRegistration(Delegate handler, bool singleObjectParam)
public ParameterInfo[] Parameters { get; }
public MethodInfo? ValueTaskAsTaskMethod { get; }
public MethodInfo? TaskResultGetter { get; }
+ public Type? ResultType { get; }
}
private static MethodInfo GetMethodFromGenericMethodDefinition(Type specializedType, MethodInfo genericMethodDefinition)
diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs
index a6fad7dcfa..7d076d14b0 100644
--- a/dotnet/src/Session.cs
+++ b/dotnet/src/Session.cs
@@ -82,6 +82,7 @@ private sealed record EventSubscription(Type EventType, Action Han
private IReadOnlyList _openCanvases = Array.Empty();
private int _isDisposed;
+ private string? _gitHubTokenProviderRegistrationId;
///
/// Channel that serializes event dispatch. enqueues;
@@ -203,6 +204,19 @@ internal void RemoveFromClient()
((ICollection>)_parentClient._sessions).Remove(new(SessionId, this));
}
+ internal void SetGitHubTokenProviderRegistration(string registrationId)
+ {
+ _gitHubTokenProviderRegistrationId = registrationId;
+ }
+
+ internal void ReleaseGitHubTokenProviderRegistration()
+ {
+ if (Interlocked.Exchange(ref _gitHubTokenProviderRegistrationId, null) is { } registrationId)
+ {
+ _parentClient.UnregisterGitHubTokenProvider(registrationId);
+ }
+ }
+
internal void StartProcessingEvents()
{
_ = ProcessEventsAsync();
@@ -357,7 +371,7 @@ void Handler(SessionEvent evt)
}
break;
- case SessionIdleEvent:
+ case SessionIdleEvent idleEvent when idleEvent.Data.Mode != SessionMode.Autopilot:
LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null,
"CopilotSession.SendAndWaitAsync idle received. Elapsed={Elapsed}, SessionId={SessionId}",
totalTimestamp,
@@ -1936,6 +1950,7 @@ await InvokeRpcAsync