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( } finally { + ReleaseGitHubTokenProviderRegistration(); RemoveFromClient(); GC.SuppressFinalize(this); } diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index c0810b3870..6ed05e3064 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -3052,15 +3052,14 @@ public sealed class GitHubMcpToolConfig public bool? DisableFormDeferral { get; set; } } -/// -/// Controls whether bypass-permissions mode is available in a managed session. -/// -[JsonConverter(typeof(JsonStringEnumConverter))] -public enum DisableBypassPermissionsMode +/// Well-known managed bypass-permissions policies. +public static class DisableBypassPermissionsModes { - /// Turn off bypass-permissions mode. - [JsonStringEnumMemberName("disable")] - Disable + /// Turns off bypass-permissions mode entirely. + public const string Disable = "disable"; + + /// Permits automatic bypass but blocks full allow-all. + public const string AllowAutoOnly = "allow-auto-only"; } /// @@ -3071,18 +3070,19 @@ public enum DisableBypassPermissionsMode /// This layer composes restrictively with any server- or device-level managed /// settings: and rules are unioned across /// layers, every present list must admit a tool for it to be -/// allowed, and is honored if any -/// layer sets it (deny-wins). +/// allowed, and policies compose to +/// the most restrictive setting. /// public sealed class ManagedSettingsPermissions { /// - /// When set to "disable", bypass-permissions mode is turned off for the - /// session regardless of other layers. Serialized as - /// disableBypassPermissionsMode. + /// Restricts bypass-permissions mode for the session regardless of other + /// layers. See for well-known + /// values. Unknown values are forwarded so newer runtime policies fail closed. + /// Serialized as disableBypassPermissionsMode. /// [JsonPropertyName("disableBypassPermissionsMode")] - public DisableBypassPermissionsMode? DisableBypassPermissionsMode { get; set; } + public string? DisableBypassPermissionsMode { get; set; } /// Tool-permission patterns that are always denied. [JsonPropertyName("deny")] @@ -3142,6 +3142,7 @@ protected SessionConfigBase(SessionConfigBase? other) DefaultAgent = other.DefaultAgent; Agent = other.Agent; DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null; + IncludedBuiltinSkills = other.IncludedBuiltinSkills is not null ? [.. other.IncludedBuiltinSkills] : null; DisabledMcpServers = other.DisabledMcpServers is not null ? [.. other.DisabledMcpServers] : null; EnableCitations = other.EnableCitations; EnableFileChangeTracking = other.EnableFileChangeTracking; @@ -3206,6 +3207,7 @@ protected SessionConfigBase(SessionConfigBase? other) ContextTier = other.ContextTier; CreateSessionFsProvider = other.CreateSessionFsProvider; GitHubToken = other.GitHubToken; + GitHubTokenProvider = other.GitHubTokenProvider; RemoteSession = other.RemoteSession; ExpAssignments = other.ExpAssignments; EnableManagedSettings = other.EnableManagedSettings; @@ -3352,6 +3354,14 @@ protected SessionConfigBase(SessionConfigBase? other) /// public bool? EnableSkills { get; set; } + /// + /// Built-in skill names to include in the session. In + /// , omitting this option excludes all + /// runtime-bundled skills; specifying names opts those built-ins back in. + /// Skills from other sources remain eligible. + /// + public IList? IncludedBuiltinSkills { get; set; } + /// /// Custom tool declarations available to the language model during the session. /// Declarations backed by an are invoked automatically; declarations without one @@ -3649,6 +3659,16 @@ protected SessionConfigBase(SessionConfigBase? other) /// public string? GitHubToken { get; set; } + /// + /// Gets or sets a callback that acquires session-scoped GitHub tokens on + /// demand. Initial cancellation, callback errors, and invalid token responses + /// reject session creation or resume instead of falling back to ambient + /// authentication. This cannot be combined with . + /// + [Experimental(Diagnostics.Experimental)] + [JsonIgnore] + public Func>? GitHubTokenProvider { get; set; } + /// /// Per-session remote behavior control: /// diff --git a/dotnet/test/E2E/RpcSessionStateE2ETests.cs b/dotnet/test/E2E/RpcSessionStateE2ETests.cs index 6dce3c250f..803c1c602b 100644 --- a/dotnet/test/E2E/RpcSessionStateE2ETests.cs +++ b/dotnet/test/E2E/RpcSessionStateE2ETests.cs @@ -451,7 +451,7 @@ public async Task Should_Set_Auth_Credentials() }); var login = $"sdk-rpc-{Guid.NewGuid():N}"; - var setCredentials = await session.Rpc.GitHubAuth.SetCredentialsAsync(new AuthInfoUser + var setCredentials = await session.Rpc.GitHubAuth.SetCredentialsAsync(new SettableAuthInfoUser { CopilotUser = new CopilotUserResponse { diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index a561ee44b2..b61546c650 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -3,6 +3,7 @@ *--------------------------------------------------------------------------------------------*/ #if NET8_0_OR_GREATER +using System.Collections.Concurrent; using System.Net; using System.Net.Sockets; using System.Diagnostics; @@ -19,6 +20,192 @@ public sealed class ClientSessionLifetimeTests { private sealed record RpcRequestRecord(string Method, JsonElement Params); + [Theory] + [InlineData("static")] + [InlineData("")] + public async Task GitHubTokenProvider_Is_Mutually_Exclusive_With_Static_Token(string staticToken) + { + await using var client = new CopilotClient(); + var config = new SessionConfig + { + GitHubToken = staticToken, + GitHubTokenProvider = _ => Task.FromResult(GitHubTokenProviderResult.Cancel()) + }; + + var error = await Assert.ThrowsAsync(() => client.CreateSessionAsync(config)); + + Assert.Contains("cannot be used together", error.Message); + } + + [Fact] + public async Task GitHubTokenProvider_Is_Released_When_Session_Is_Deleted() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var session = await client.CreateSessionAsync(new SessionConfig + { + GitHubTokenProvider = _ => Task.FromResult(GitHubTokenProviderResult.Cancel()) + }); + var registrationId = Assert.Single(server.Requests, request => request.Method == "session.create") + .Params.GetProperty("gitHubTokenProviderRegistrationId").GetString(); + + await client.DeleteSessionAsync(session.SessionId); + + var error = await Assert.ThrowsAsync(() => + server.SendRequestAsync("gitHubToken.getToken", TokenRequest(registrationId))); + Assert.Contains("Unknown GitHub token provider registration ID", error.Message); + await session.DisposeAsync(); + } + + [Fact] + public async Task GitHubTokenProvider_Is_Serialized_And_Maps_Callbacks() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + GitHubTokenProviderArgs? callbackArgs = null; + var session = await client.CreateSessionAsync(new SessionConfig + { + GitHubTokenProvider = args => + { + callbackArgs = args; + return Task.FromResult(GitHubTokenProviderResult.FromToken(new GitHubToken + { + AccessToken = "secret-token", + TokenType = "bearer", + ExpiresIn = 8 * 60 * 60 + })); + } + }); + var request = Assert.Single(server.Requests, request => request.Method == "session.create"); + var registrationId = request.Params.GetProperty("gitHubTokenProviderRegistrationId").GetString(); + Assert.False(string.IsNullOrEmpty(registrationId)); + Assert.False(request.Params.TryGetProperty("gitHubToken", out _)); + + var result = await server.SendRequestAsync("gitHubToken.getToken", new Dictionary + { + ["registrationId"] = registrationId, + ["host"] = "github.example.com", + ["sessionId"] = session.SessionId, + ["reason"] = "refresh" + }); + + Assert.True(result.TryGetProperty("kind", out var kind), result.ToString()); + Assert.Equal("token", kind.GetString()); + Assert.Equal("secret-token", result.GetProperty("accessToken").GetString()); + Assert.Equal(8 * 60 * 60, result.GetProperty("expiresIn").GetInt64()); + Assert.NotNull(callbackArgs); + Assert.Equal("github.example.com", callbackArgs.Host); + Assert.Equal(session.SessionId, callbackArgs.SessionId); + Assert.Equal(GitHubTokenRequestReason.Refresh, callbackArgs.Reason); + Assert.DoesNotContain("secret-token", new GitHubToken + { + AccessToken = "secret-token", + ExpiresIn = 8 * 60 * 60 + }.ToString()); + + await session.DisposeAsync(); + var error = await Assert.ThrowsAsync(() => + server.SendRequestAsync("gitHubToken.getToken", new Dictionary + { + ["registrationId"] = registrationId, + ["host"] = "github.com", + ["reason"] = "initial" + })); + Assert.Contains("Unknown GitHub token provider registration ID", error.Message); + + server.ClearRequests(); + var resumed = await client.ResumeSessionAsync("resumed-session", new ResumeSessionConfig + { + GitHubTokenProvider = _ => Task.FromResult(GitHubTokenProviderResult.Cancel()) + }); + var resumeRequest = Assert.Single(server.Requests, request => request.Method == "session.resume"); + Assert.False(string.IsNullOrEmpty( + resumeRequest.Params.GetProperty("gitHubTokenProviderRegistrationId").GetString())); + await resumed.DisposeAsync(); + } + + [Fact] + public async Task GitHubTokenProvider_Handles_Cancellation_Errors_And_Rollback() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var cancelledSession = await client.CreateSessionAsync(new SessionConfig + { + GitHubTokenProvider = _ => Task.FromResult(GitHubTokenProviderResult.Cancel()) + }); + var cancelledId = Assert.Single(server.Requests, request => request.Method == "session.create") + .Params.GetProperty("gitHubTokenProviderRegistrationId").GetString(); + var cancelled = await server.SendRequestAsync("gitHubToken.getToken", TokenRequest(cancelledId)); + Assert.True(cancelled.TryGetProperty("kind", out var cancelledKind), cancelled.ToString()); + Assert.Equal("cancelled", cancelledKind.GetString()); + await cancelledSession.DisposeAsync(); + + server.ClearRequests(); + var providerSession = await client.CreateSessionAsync(new SessionConfig + { + GitHubTokenProvider = _ => Task.FromException( + new InvalidOperationException("provider failed")) + }); + var providerId = Assert.Single(server.Requests, request => request.Method == "session.create") + .Params.GetProperty("gitHubTokenProviderRegistrationId").GetString(); + var callbackError = await Assert.ThrowsAsync(() => + server.SendRequestAsync("gitHubToken.getToken", TokenRequest(providerId))); + Assert.Contains("provider failed", callbackError.Message); + await providerSession.DisposeAsync(); + + server.ClearRequests(); + server.FailSessionCreate(); + await Assert.ThrowsAsync(() => client.CreateSessionAsync(new SessionConfig + { + GitHubTokenProvider = _ => Task.FromResult(GitHubTokenProviderResult.Cancel()) + })); + var rolledBackId = Assert.Single(server.Requests, request => request.Method == "session.create") + .Params.GetProperty("gitHubTokenProviderRegistrationId").GetString(); + var rollbackError = await Assert.ThrowsAsync(() => + server.SendRequestAsync("gitHubToken.getToken", TokenRequest(rolledBackId))); + Assert.Contains("Unknown GitHub token provider registration ID", rollbackError.Message); + } + + [Fact] + public async Task GitHubTokenProvider_Resume_Replaces_Ownership() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var first = await client.CreateSessionAsync(new SessionConfig + { + SessionId = "replacement-session", + GitHubTokenProvider = _ => Task.FromResult(GitHubTokenProviderResult.Cancel()) + }); + var firstId = Assert.Single(server.Requests, request => request.Method == "session.create") + .Params.GetProperty("gitHubTokenProviderRegistrationId").GetString(); + + await first.DisposeAsync(); + await Assert.ThrowsAsync(() => + server.SendRequestAsync("gitHubToken.getToken", TokenRequest(firstId))); + + server.ClearRequests(); + var resumed = await client.ResumeSessionAsync("replacement-session", new ResumeSessionConfig + { + GitHubTokenProvider = _ => Task.FromResult(GitHubTokenProviderResult.Cancel()) + }); + var secondId = Assert.Single(server.Requests, request => request.Method == "session.resume") + .Params.GetProperty("gitHubTokenProviderRegistrationId").GetString(); + + var result = await server.SendRequestAsync("gitHubToken.getToken", TokenRequest(secondId)); + Assert.Equal("cancelled", result.GetProperty("kind").GetString()); + + await resumed.DisposeAsync(); + await Assert.ThrowsAsync(() => + server.SendRequestAsync("gitHubToken.getToken", TokenRequest(secondId))); + } + + private static Dictionary TokenRequest(string? registrationId) => new() + { + ["registrationId"] = registrationId, + ["host"] = "github.com", + ["reason"] = "initial" + }; + [Fact] public async Task StopAsync_Requests_Runtime_Shutdown_For_Owned_Process() { @@ -342,6 +529,146 @@ public async Task SessionRequests_Serialize_Terminal_Tools() Assert.True(resumeRequest.Params.GetProperty("tools")[0].GetProperty("isTerminal").GetBoolean()); } + [Fact] + public async Task EmptyMode_Create_Sends_Empty_IncludedBuiltinSkills() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + Mode = CopilotClientMode.Empty, + BaseDirectory = Path.GetTempPath(), + }); + + await using var created = await client.CreateSessionAsync(new SessionConfig + { + AvailableTools = [], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var update = Assert.Single(server.Requests, request => request.Method == "session.options.update"); + Assert.True(update.Params.TryGetProperty("includedBuiltinSkills", out var skills)); + Assert.Equal(JsonValueKind.Array, skills.ValueKind); + Assert.Equal(0, skills.GetArrayLength()); + // Adjacent unconditional plugin isolation is still present. + Assert.True(update.Params.TryGetProperty("installedPlugins", out var plugins)); + Assert.Equal(0, plugins.GetArrayLength()); + } + + [Fact] + public async Task EmptyMode_Resume_Sends_Empty_IncludedBuiltinSkills() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + Mode = CopilotClientMode.Empty, + BaseDirectory = Path.GetTempPath(), + }); + + await using var resumed = await client.ResumeSessionAsync("resume-empty-skills", new ResumeSessionConfig + { + AvailableTools = [], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var update = Assert.Single(server.Requests, request => request.Method == "session.options.update"); + Assert.True(update.Params.TryGetProperty("includedBuiltinSkills", out var skills)); + Assert.Equal(JsonValueKind.Array, skills.ValueKind); + Assert.Equal(0, skills.GetArrayLength()); + } + + [Fact] + public async Task EmptyMode_Resume_Preserves_Explicit_IncludedBuiltinSkills() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + Mode = CopilotClientMode.Empty, + BaseDirectory = Path.GetTempPath(), + }); + + await using var resumed = await client.ResumeSessionAsync("resume-selected-skills", new ResumeSessionConfig + { + AvailableTools = [], + IncludedBuiltinSkills = ["code-review"], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var update = Assert.Single(server.Requests, request => request.Method == "session.options.update"); + var skills = update.Params.GetProperty("includedBuiltinSkills"); + Assert.Equal(["code-review"], skills.EnumerateArray().Select(value => value.GetString())); + } + + [Fact] + public async Task EmptyMode_Create_With_EnableSkills_Still_Sends_Empty_IncludedBuiltinSkills() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + Mode = CopilotClientMode.Empty, + BaseDirectory = Path.GetTempPath(), + }); + + // Caller opts into their own custom skills. Runtime-bundled built-ins must + // still be excluded: the empty post-patch cannot be weakened by the caller. + await using var created = await client.CreateSessionAsync(new SessionConfig + { + AvailableTools = [], + EnableSkills = true, + SkillDirectories = [Path.Combine(Path.GetTempPath(), "skills")], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var update = Assert.Single(server.Requests, request => request.Method == "session.options.update"); + Assert.True(update.Params.TryGetProperty("includedBuiltinSkills", out var skills)); + Assert.Equal(JsonValueKind.Array, skills.ValueKind); + Assert.Equal(0, skills.GetArrayLength()); + } + + [Fact] + public async Task EmptyMode_Create_Preserves_Explicit_IncludedBuiltinSkills() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + Mode = CopilotClientMode.Empty, + BaseDirectory = Path.GetTempPath(), + }); + + await using var created = await client.CreateSessionAsync(new SessionConfig + { + AvailableTools = [], + IncludedBuiltinSkills = ["code-review"], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var update = Assert.Single(server.Requests, request => request.Method == "session.options.update"); + var skills = update.Params.GetProperty("includedBuiltinSkills"); + Assert.Equal(["code-review"], skills.EnumerateArray().Select(value => value.GetString())); + } + + [Fact] + public async Task CopilotCliMode_Create_Does_Not_Inject_IncludedBuiltinSkills() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + + await using var created = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + // In the default copilot-cli mode with no overridable options set, no + // options patch is sent at all, so the field is never injected. + Assert.DoesNotContain(server.Requests, request => + request.Method == "session.options.update" + && request.Params.TryGetProperty("includedBuiltinSkills", out _)); + } + [Fact] public async Task CreateSessionAsync_Registers_McpAuth_Interest_Only_When_Handler_Configured() { @@ -502,6 +829,66 @@ public async Task Generated_Session_Rpc_Throws_When_Session_Disposed() await Assert.ThrowsAsync(() => session.Rpc.Model.GetCurrentAsync()); } + [Fact] + public async Task SendAndWaitAsync_Skips_Autopilot_Continuation_Idle() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var sendTask = session.SendAndWaitAsync(new MessageOptions { Prompt = "keep going" }); + await WaitForRequestAsync(server, "session.send"); + + var continuationIdleProcessed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var subscription = session.On(idle => + { + if (idle.Data.Mode == SessionMode.Autopilot) + { + continuationIdleProcessed.TrySetResult(); + } + }); + + DispatchEvent(session, new AssistantMessageEvent + { + Id = Guid.NewGuid(), + Data = new AssistantMessageData + { + Content = "intermediate", + MessageId = "assistant-1" + } + }); + DispatchEvent(session, new SessionIdleEvent + { + Id = Guid.NewGuid(), + Data = new SessionIdleData { Mode = SessionMode.Autopilot } + }); + + await continuationIdleProcessed.Task.WaitAsync(TimeSpan.FromSeconds(5)); + Assert.False(sendTask.IsCompleted); + + DispatchEvent(session, new AssistantMessageEvent + { + Id = Guid.NewGuid(), + Data = new AssistantMessageData + { + Content = "final", + MessageId = "assistant-2" + } + }); + DispatchEvent(session, new SessionIdleEvent + { + Id = Guid.NewGuid(), + Data = new SessionIdleData { Mode = SessionMode.Interactive } + }); + + var result = await sendTask.WaitAsync(TimeSpan.FromSeconds(5)); + Assert.NotNull(result); + Assert.Equal("final", result.Data.Content); + } + [MethodImpl(MethodImplOptions.NoInlining)] private static async Task> CreateDroppedSessionAsync(CopilotClient client) { @@ -552,7 +939,7 @@ public async Task CreateSessionAsync_Serializes_ManagedSettings_Permissions() { Permissions = new ManagedSettingsPermissions { - DisableBypassPermissionsMode = DisableBypassPermissionsMode.Disable, + DisableBypassPermissionsMode = DisableBypassPermissionsModes.Disable, Deny = ["shell(rm*)"], Ask = ["write"], Allow = [] @@ -585,6 +972,32 @@ public async Task CreateSessionAsync_Serializes_ManagedSettings_Permissions() Assert.True(invocation.ManagedSettingsEnabled); } + [Fact] + public async Task CreateSessionAsync_Serializes_Future_ManagedSettings_Bypass_Mode() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await client.StartAsync(); + + await using var session = await client.CreateSessionAsync(new SessionConfig + { + ManagedSettings = new ManagedSettings + { + Permissions = new ManagedSettingsPermissions + { + DisableBypassPermissionsMode = "future-fail-closed-mode" + } + }, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var request = Assert.Single(server.Requests, request => request.Method == "session.create"); + var permissions = request.Params.GetProperty("managedSettings").GetProperty("permissions"); + Assert.Equal( + "future-fail-closed-mode", + permissions.GetProperty("disableBypassPermissionsMode").GetString()); + } + [Fact] public async Task PermissionResponse_Forwards_DecisionContext_As_Sibling_Of_Result() { @@ -892,9 +1305,13 @@ private sealed class FakeCopilotServer : IAsyncDisposable private readonly Task _serverTask; private readonly List _requests = []; private readonly object _requestsLock = new(); + private readonly ConcurrentDictionary> _pendingRequests = new(); + private NetworkStream? _stream; + private int _nextRequestId; private string? _lastSessionId; private bool _delayDestroy; private bool _failRuntimeShutdown; + private bool _failSessionCreate; private FakeCopilotServer(TcpListener listener) { @@ -956,6 +1373,31 @@ public void FailRuntimeShutdown() _failRuntimeShutdown = true; } + public void FailSessionCreate() + { + _failSessionCreate = true; + } + + public async Task SendRequestAsync(string method, Dictionary parameters) + { + var stream = _stream ?? throw new InvalidOperationException("Client is not connected."); + var id = Interlocked.Increment(ref _nextRequestId); + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + if (!_pendingRequests.TryAdd(id, completion)) + { + throw new InvalidOperationException("Failed to track callback request."); + } + + await WriteMessageAsync(stream, new Dictionary + { + ["jsonrpc"] = "2.0", + ["id"] = id, + ["method"] = method, + ["params"] = parameters + }, _cts.Token); + return await completion.Task.WaitAsync(_cts.Token); + } + public async ValueTask DisposeAsync() { _allowDestroy.TrySetResult(); @@ -978,16 +1420,37 @@ private async Task RunAsync() { using var tcpClient = await _listener.AcceptTcpClientAsync(_cts.Token); using var stream = tcpClient.GetStream(); + _stream = stream; while (!_cts.Token.IsCancellationRequested) { - using var request = await ReadMessageAsync(stream, _cts.Token); - if (request is null) + using var message = await ReadMessageAsync(stream, _cts.Token); + if (message is null) { return; } - await HandleRequestAsync(stream, request.RootElement, _cts.Token); + var root = message.RootElement; + if (root.TryGetProperty("method", out _)) + { + await HandleRequestAsync(stream, root, _cts.Token); + continue; + } + + if (root.TryGetProperty("id", out var responseId) + && responseId.TryGetInt32(out var id) + && _pendingRequests.TryRemove(id, out var completion)) + { + if (root.TryGetProperty("error", out var error)) + { + completion.TrySetException(new InvalidOperationException( + error.GetProperty("message").GetString())); + } + else + { + completion.TrySetResult(root.GetProperty("result").Clone()); + } + } } } @@ -1023,6 +1486,21 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel { _requests.Add(new RpcRequestRecord(method!, paramsElement)); } + if (method == "session.create" && _failSessionCreate) + { + _failSessionCreate = false; + await WriteMessageAsync(stream, new Dictionary + { + ["jsonrpc"] = "2.0", + ["id"] = id, + ["error"] = new Dictionary + { + ["code"] = -32000, + ["message"] = "session create failed" + } + }, cancellationToken); + return; + } object? result = method switch { "connect" => new Dictionary @@ -1041,6 +1519,10 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel { ["messageId"] = "message-1" }, + "session.options.update" => new Dictionary + { + ["success"] = true + }, "session.mcp.oauth.handlePendingRequest" => new Dictionary { ["success"] = true diff --git a/go/README.md b/go/README.md index d8588699c4..ddd74b91aa 100644 --- a/go/README.md +++ b/go/README.md @@ -222,6 +222,7 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec - `InfiniteSessions` (\*InfiniteSessionConfig): Automatic context compaction configuration - `WorkingDirectory` (string): Working directory for the session (default: runtime process working directory) - `EnableSessionStore` (\*bool): Enables the cross-session store for search and retrieval across sessions. When unset in `ModeCopilotCli`, the runtime default applies (enabled). In `ModeEmpty`, defaults to disabled. +- `GitHubTokenProvider` (GitHubTokenProvider): Acquires session-scoped GitHub tokens on demand. Return `GitHubTokenResult` with a positive `ExpiresIn` value (production GitHub tokens typically use `8 * 60 * 60` seconds), or `GitHubTokenCancelled`. Cannot be combined with `GitHubToken`. - `OnPermissionRequest` (PermissionHandlerFunc): Optional handler called before each tool execution to approve or deny it. When nil, permission requests are emitted as events and left pending for manual resolution. `copilot.PermissionHandler.ApproveAll` approves requests when managed settings are disabled and returns an error when `EnableManagedSettings` is true. Custom handlers can inspect `RequiresManagedApproval()` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section. - `OnUserInputRequest` (UserInputHandler): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section. - `Hooks` (\*SessionHooks): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. @@ -237,6 +238,24 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec - `Streaming` (*bool): Enable streaming delta events (nil = runtime default) - `Commands` ([]CommandDefinition): Slash-commands. See [Commands](#commands) section. - `OnElicitationRequest` (ElicitationHandler): Elicitation handler. See [Elicitation Requests](#elicitation-requests-serverclient) section. +- `GitHubTokenProvider` (GitHubTokenProvider): Replaces the session-scoped token provider when resuming. Cannot be combined with `GitHubToken`. + +```go +session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + GitHubTokenProvider: func(args copilot.GitHubTokenProviderArgs) (*copilot.GitHubTokenProviderResult, error) { + token, err := acquireToken(args.Host) + if err != nil { + return nil, err + } + return copilot.GitHubTokenResult(&copilot.GitHubToken{ + AccessToken: token, + ExpiresIn: 8 * 60 * 60, + }), nil + }, +}) +``` + +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. ### Session diff --git a/go/client.go b/go/client.go index fb02897f91..4e44696a55 100644 --- a/go/client.go +++ b/go/client.go @@ -145,19 +145,23 @@ func validateEnvironmentOptions(connection RuntimeConnection, opts *ClientOption // } // defer client.Stop() type Client struct { - options ClientOptions - process *exec.Cmd - client *jsonrpc2.Client - actualPort int - actualHost string - state connectionState - sessions map[string]*Session - sessionsMux sync.Mutex - isExternalServer bool - conn net.Conn // stores net.Conn for external TCP connections - useStdio bool // resolved value from options - useInProcess bool // true for InProcessConnection (FFI transport) - ffiHost inProcessHost + options ClientOptions + process *exec.Cmd + client *jsonrpc2.Client + actualPort int + actualHost string + state connectionState + sessions map[string]*Session + sessionsMux sync.Mutex + gitHubTokenProviders map[string]GitHubTokenProvider + gitHubTokenProvidersMux sync.RWMutex + sessionOperations map[string]*sessionOperation + sessionOperationsMux sync.Mutex + isExternalServer bool + conn net.Conn // stores net.Conn for external TCP connections + useStdio bool // resolved value from options + useInProcess bool // true for InProcessConnection (FFI transport) + ffiHost inProcessHost // resolved process options for the spawned runtime (zero values for URIConnection) cliPath string cliArgs []string @@ -189,6 +193,11 @@ type Client struct { internalRPC *rpc.InternalServerRPC } +type sessionOperation struct { + mutex sync.Mutex + users int +} + // NewClient creates a new Copilot runtime client with the given options. // // If options is nil, default options are used (spawns the bundled runtime over @@ -215,12 +224,13 @@ func NewClient(options *ClientOptions) *Client { opts := ClientOptions{} client := &Client{ - options: opts, - state: stateDisconnected, - sessions: make(map[string]*Session), - actualHost: "localhost", - isExternalServer: false, - useStdio: true, + options: opts, + state: stateDisconnected, + sessions: make(map[string]*Session), + gitHubTokenProviders: make(map[string]GitHubTokenProvider), + actualHost: "localhost", + isExternalServer: false, + useStdio: true, } if options != nil { @@ -548,6 +558,7 @@ func (c *Client) Stop() error { c.sessionsMux.Lock() c.sessions = make(map[string]*Session) c.sessionsMux.Unlock() + c.clearGitHubTokenProviders() c.startStopMux.Lock() defer c.startStopMux.Unlock() @@ -663,6 +674,7 @@ func (c *Client) ForceStop() { c.sessionsMux.Lock() c.sessions = make(map[string]*Session) c.sessionsMux.Unlock() + c.clearGitHubTokenProviders() c.startStopMux.Lock() defer c.startStopMux.Unlock() @@ -780,6 +792,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config == nil { config = &SessionConfig{} } + if config.GitHubToken != "" && config.GitHubTokenProvider != nil { + return nil, fmt.Errorf("GitHubToken and GitHubTokenProvider cannot be used together") + } if err := c.ensureConnected(ctx); err != nil { return nil, err @@ -787,6 +802,14 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses c.applyConfigDefaultsForMode(config) + registrationID := c.registerGitHubTokenProvider(config.GitHubTokenProvider) + registrationTransferred := false + defer func() { + if !registrationTransferred { + c.unregisterGitHubTokenProvider(registrationID) + } + }() + req := createSessionRequest{} req.Model = config.Model req.ClientName = config.ClientName @@ -849,6 +872,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.ToolSearch = config.ToolSearch req.Memory = config.Memory req.GitHubToken = config.GitHubToken + req.GitHubTokenProviderRegistrationID = registrationID req.RemoteSession = config.RemoteSession req.Cloud = config.Cloud req.Canvases = config.Canvases @@ -1099,10 +1123,17 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses CustomAgentsLocalOnly: config.CustomAgentsLocalOnly, CoauthorEnabled: config.CoauthorEnabled, ManageScheduleEnabled: config.ManageScheduleEnabled, + IncludedBuiltinSkills: config.IncludedBuiltinSkills, }); err != nil { return nil, err } + if registrationID != "" { + session.setGitHubTokenProviderRegistrationRelease(func() { + c.unregisterGitHubTokenProvider(registrationID) + }) + registrationTransferred = true + } return session, nil } @@ -1130,9 +1161,15 @@ func (c *Client) ResumeSession(ctx context.Context, sessionID string, config *Re // Tools: []copilot.Tool{myNewTool}, // }) func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, config *ResumeSessionConfig) (*Session, error) { + unlockSession := c.lockSessionOperation(sessionID) + defer unlockSession() + if config == nil { config = &ResumeSessionConfig{} } + if config.GitHubToken != "" && config.GitHubTokenProvider != nil { + return nil, fmt.Errorf("GitHubToken and GitHubTokenProvider cannot be used together") + } if err := c.ensureConnected(ctx); err != nil { return nil, err @@ -1140,6 +1177,14 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, c.applyResumeDefaultsForMode(config) + registrationID := c.registerGitHubTokenProvider(config.GitHubTokenProvider) + registrationTransferred := false + defer func() { + if !registrationTransferred { + c.unregisterGitHubTokenProvider(registrationID) + } + }() + var req resumeSessionRequest req.SessionID = sessionID req.ClientName = config.ClientName @@ -1233,6 +1278,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.ToolSearch = config.ToolSearch req.Memory = config.Memory req.GitHubToken = config.GitHubToken + req.GitHubTokenProviderRegistrationID = registrationID req.RemoteSession = config.RemoteSession req.Canvases = config.Canvases req.OpenCanvases = config.OpenCanvases @@ -1318,22 +1364,31 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, } c.sessionsMux.Lock() + replacedSession := c.sessions[sessionID] c.sessions[sessionID] = session c.sessionsMux.Unlock() + restoreReplacedSession := func() { + c.sessionsMux.Lock() + if current := c.sessions[sessionID]; current == nil || current == session { + if replacedSession != nil { + c.sessions[sessionID] = replacedSession + } else { + delete(c.sessions, sessionID) + } + } + c.sessionsMux.Unlock() + } + if c.options.SessionFS != nil { if config.CreateSessionFSProvider == nil { - c.sessionsMux.Lock() - delete(c.sessions, sessionID) - c.sessionsMux.Unlock() + restoreReplacedSession() return nil, fmt.Errorf("CreateSessionFSProvider is required in session config when SessionFS is enabled in client options") } provider := config.CreateSessionFSProvider(session) if c.options.SessionFS.Capabilities != nil && c.options.SessionFS.Capabilities.Sqlite { if _, ok := provider.(SessionFSSqliteProvider); !ok { - c.sessionsMux.Lock() - delete(c.sessions, sessionID) - c.sessionsMux.Unlock() + restoreReplacedSession() return nil, fmt.Errorf("SessionFS capabilities declare SQLite support but the provider does not implement SessionFSSqliteProvider") } } @@ -1342,17 +1397,13 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, result, err := c.client.Request(ctx, "session.resume", req) if err != nil { - c.sessionsMux.Lock() - delete(c.sessions, sessionID) - c.sessionsMux.Unlock() + restoreReplacedSession() return nil, fmt.Errorf("failed to resume session: %w", err) } var response resumeSessionResponse if err := json.Unmarshal(result, &response); err != nil { - c.sessionsMux.Lock() - delete(c.sessions, sessionID) - c.sessionsMux.Unlock() + restoreReplacedSession() return nil, fmt.Errorf("failed to unmarshal response: %w", err) } @@ -1361,9 +1412,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, "sessionId": sessionID, "eventType": "mcp.oauth_required", }); err != nil { - c.sessionsMux.Lock() - delete(c.sessions, sessionID) - c.sessionsMux.Unlock() + restoreReplacedSession() return nil, err } } @@ -1377,10 +1426,21 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, CustomAgentsLocalOnly: config.CustomAgentsLocalOnly, CoauthorEnabled: config.CoauthorEnabled, ManageScheduleEnabled: config.ManageScheduleEnabled, + IncludedBuiltinSkills: config.IncludedBuiltinSkills, }); err != nil { + restoreReplacedSession() return nil, err } + if registrationID != "" { + session.setGitHubTokenProviderRegistrationRelease(func() { + c.unregisterGitHubTokenProvider(registrationID) + }) + registrationTransferred = true + } + if replacedSession != nil && replacedSession != session { + replacedSession.releaseGitHubTokenProviderRegistration() + } return session, nil } @@ -1472,6 +1532,9 @@ func (c *Client) GetSessionMetadata(ctx context.Context, sessionID string) (*Ses // log.Fatal(err) // } func (c *Client) DeleteSession(ctx context.Context, sessionID string) error { + unlockSession := c.lockSessionOperation(sessionID) + defer unlockSession() + if err := c.ensureConnected(ctx); err != nil { return err } @@ -1496,8 +1559,12 @@ func (c *Client) DeleteSession(ctx context.Context, sessionID string) error { // Remove from local sessions map if present c.sessionsMux.Lock() + session := c.sessions[sessionID] delete(c.sessions, sessionID) c.sessionsMux.Unlock() + if session != nil { + session.releaseGitHubTokenProviderRegistration() + } return nil } @@ -2039,15 +2106,7 @@ func (c *Client) startCLIServer(ctx context.Context) error { // Create JSON-RPC client immediately c.client = jsonrpc2.NewClient(stdin, stdout) c.client.SetProcessDone(c.processDone, c.processErrorPtr) - c.client.SetOnClose(func() { - // Run in a goroutine to avoid deadlocking with Stop/ForceStop, - // which hold startStopMux while waiting for readLoop to finish. - go func() { - c.startStopMux.Lock() - defer c.startStopMux.Unlock() - c.state = stateDisconnected - }() - }) + c.client.SetOnClose(c.handleConnectionClose) c.RPC = rpc.NewServerRPC(c.client) c.internalRPC = rpc.NewInternalServerRPC(c.client) c.setupNotificationHandler() @@ -2166,15 +2225,7 @@ func (c *Client) startInProcess(ctx context.Context) error { } c.client = jsonrpc2.NewClient(host.Writer(), host.Reader()) - c.client.SetOnClose(func() { - // Run in a goroutine to avoid deadlocking with Stop/ForceStop, which hold - // startStopMux while waiting for readLoop to finish. - go func() { - c.startStopMux.Lock() - defer c.startStopMux.Unlock() - c.state = stateDisconnected - }() - }) + c.client.SetOnClose(c.handleConnectionClose) c.RPC = rpc.NewServerRPC(c.client) c.internalRPC = rpc.NewInternalServerRPC(c.client) c.setupNotificationHandler() @@ -2324,13 +2375,7 @@ func (c *Client) connectViaTCP(ctx context.Context) error { if c.processDone != nil { c.client.SetProcessDone(c.processDone, c.processErrorPtr) } - c.client.SetOnClose(func() { - go func() { - c.startStopMux.Lock() - defer c.startStopMux.Unlock() - c.state = stateDisconnected - }() - }) + c.client.SetOnClose(c.handleConnectionClose) c.RPC = rpc.NewServerRPC(c.client) c.internalRPC = rpc.NewInternalServerRPC(c.client) c.setupNotificationHandler() @@ -2361,8 +2406,10 @@ func (c *Client) setupNotificationHandler() { // payload's sessionId. Always register the global handlers so the generated // hooks.invoke handler is wired to our dispatcher. handlers := &rpc.ClientGlobalAPIHandlers{ - Hooks: &hooksAdapter{client: c}, + Hooks: &hooksAdapter{client: c}, + GitHubToken: &gitHubTokenAdapter{client: c}, } + if c.options.RequestHandler != nil { handlers.LlmInference = newCopilotRequestAdapter(c.options.RequestHandler, func() *rpc.ServerLlmInferenceAPI { if c.RPC == nil { @@ -2377,6 +2424,107 @@ func (c *Client) setupNotificationHandler() { rpc.RegisterClientGlobalAPIHandlers(c.client, handlers) } +func (c *Client) registerGitHubTokenProvider(provider GitHubTokenProvider) string { + if provider == nil { + return "" + } + registrationID := uuid.NewString() + c.gitHubTokenProvidersMux.Lock() + if c.gitHubTokenProviders == nil { + c.gitHubTokenProviders = make(map[string]GitHubTokenProvider) + } + c.gitHubTokenProviders[registrationID] = provider + c.gitHubTokenProvidersMux.Unlock() + return registrationID +} + +func (c *Client) unregisterGitHubTokenProvider(registrationID string) { + if registrationID == "" { + return + } + c.gitHubTokenProvidersMux.Lock() + delete(c.gitHubTokenProviders, registrationID) + c.gitHubTokenProvidersMux.Unlock() +} + +func (c *Client) clearGitHubTokenProviders() { + c.gitHubTokenProvidersMux.Lock() + c.gitHubTokenProviders = make(map[string]GitHubTokenProvider) + c.gitHubTokenProvidersMux.Unlock() +} + +func (c *Client) handleConnectionClose() { + c.clearGitHubTokenProviders() + // Avoid deadlocking with Stop/ForceStop, which hold startStopMux while + // waiting for the JSON-RPC read loop to finish. + go func() { + c.startStopMux.Lock() + defer c.startStopMux.Unlock() + c.state = stateDisconnected + }() +} + +func (c *Client) lockSessionOperation(sessionID string) func() { + c.sessionOperationsMux.Lock() + if c.sessionOperations == nil { + c.sessionOperations = make(map[string]*sessionOperation) + } + operation := c.sessionOperations[sessionID] + if operation == nil { + operation = &sessionOperation{} + c.sessionOperations[sessionID] = operation + } + operation.users++ + c.sessionOperationsMux.Unlock() + + operation.mutex.Lock() + return func() { + operation.mutex.Unlock() + c.sessionOperationsMux.Lock() + operation.users-- + if operation.users == 0 { + delete(c.sessionOperations, sessionID) + } + c.sessionOperationsMux.Unlock() + } +} + +type gitHubTokenAdapter struct { + client *Client +} + +func (a *gitHubTokenAdapter) GetToken(request *rpc.GitHubTokenAcquireRequest) (rpc.GitHubTokenAcquireResult, error) { + if request == nil { + return nil, fmt.Errorf("missing GitHub token acquire request") + } + a.client.gitHubTokenProvidersMux.RLock() + provider := a.client.gitHubTokenProviders[request.RegistrationID] + a.client.gitHubTokenProvidersMux.RUnlock() + if provider == nil { + return nil, fmt.Errorf("unknown GitHub token provider registration ID %q", request.RegistrationID) + } + + result, err := provider(GitHubTokenProviderArgs{ + Host: request.Host, + SessionID: request.SessionID, + Reason: request.Reason, + }) + if err != nil { + return nil, err + } + if result != nil && result.Cancelled { + return &rpc.GitHubTokenAcquireResultCancelled{}, nil + } + if result == nil || result.Token == nil { + return nil, fmt.Errorf("GitHub token provider returned neither a token nor cancellation") + } + return &rpc.GitHubTokenAcquireResultToken{ + AccessToken: result.Token.AccessToken, + TokenType: result.Token.TokenType, + ExpiresIn: result.Token.ExpiresIn, + }, nil +} + // gitHubTelemetryAdapter adapts the OnGitHubTelemetry option to the generated // rpc.GitHubTelemetryHandler interface. type gitHubTelemetryAdapter struct { diff --git a/go/client_test.go b/go/client_test.go index d0139eb11a..c6ab0808cb 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -2139,6 +2139,175 @@ func TestClient_MCPAuthInterestRegistration(t *testing.T) { }) } +func findRequest(requests []recordedRequest, method string) (recordedRequest, bool) { + for _, r := range requests { + if r.Method == method { + return r, true + } + } + return recordedRequest{}, false +} + +func TestClient_EmptyModeIncludedBuiltinSkills(t *testing.T) { + t.Run("create post-patch sends empty includedBuiltinSkills", func(t *testing.T) { + client, requests, cleanup := newInMemoryClientWithOptions(t, &ClientOptions{ + Mode: ModeEmpty, + BaseDirectory: "/tmp/copilot-test", + }) + defer cleanup() + + session, err := client.CreateSession(t.Context(), &SessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + OnEvent: func(SessionEvent) {}, + AvailableTools: []string{}, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + defer session.Disconnect() + + update, ok := findRequest(requests.snapshot(), "session.options.update") + if !ok { + t.Fatalf("expected session.options.update in %+v", requests.snapshot()) + } + skills, present := update.Params["includedBuiltinSkills"] + if !present { + t.Fatalf("expected includedBuiltinSkills in patch, got %+v", update.Params) + } + if arr, isArr := skills.([]any); !isArr || len(arr) != 0 { + t.Fatalf("expected includedBuiltinSkills=[], got %#v", skills) + } + }) + + t.Run("resume post-patch sends empty includedBuiltinSkills", func(t *testing.T) { + client, requests, cleanup := newInMemoryClientWithOptions(t, &ClientOptions{ + Mode: ModeEmpty, + BaseDirectory: "/tmp/copilot-test", + }) + defer cleanup() + + session, err := client.ResumeSession(t.Context(), "session-empty", &ResumeSessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + OnEvent: func(SessionEvent) {}, + AvailableTools: []string{}, + }) + if err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + defer session.Disconnect() + + update, ok := findRequest(requests.snapshot(), "session.options.update") + if !ok { + t.Fatalf("expected session.options.update in %+v", requests.snapshot()) + } + if arr, isArr := update.Params["includedBuiltinSkills"].([]any); !isArr || len(arr) != 0 { + t.Fatalf("expected includedBuiltinSkills=[], got %#v", update.Params["includedBuiltinSkills"]) + } + }) + + t.Run("resume preserves explicit built-in skill allowlist", func(t *testing.T) { + client, requests, cleanup := newInMemoryClientWithOptions(t, &ClientOptions{ + Mode: ModeEmpty, + BaseDirectory: "/tmp/copilot-test", + }) + defer cleanup() + + session, err := client.ResumeSessionWithOptions(t.Context(), "resume-skills", &ResumeSessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + AvailableTools: []string{}, + IncludedBuiltinSkills: []string{"code-review"}, + }) + if err != nil { + t.Fatalf("ResumeSessionWithOptions failed: %v", err) + } + defer session.Disconnect() + + update, ok := findRequest(requests.snapshot(), "session.options.update") + if !ok { + t.Fatalf("expected session.options.update in %+v", requests.snapshot()) + } + skills, ok := update.Params["includedBuiltinSkills"].([]any) + if !ok || len(skills) != 1 || skills[0] != "code-review" { + t.Fatalf("expected includedBuiltinSkills=[code-review], got %#v", update.Params["includedBuiltinSkills"]) + } + }) + + t.Run("caller opting into custom skills keeps includedBuiltinSkills empty", func(t *testing.T) { + client, requests, cleanup := newInMemoryClientWithOptions(t, &ClientOptions{ + Mode: ModeEmpty, + BaseDirectory: "/tmp/copilot-test", + }) + defer cleanup() + + session, err := client.CreateSession(t.Context(), &SessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + OnEvent: func(SessionEvent) {}, + AvailableTools: []string{}, + EnableSkills: Bool(true), + SkillDirectories: []string{"/tmp/custom-skills"}, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + defer session.Disconnect() + + update, ok := findRequest(requests.snapshot(), "session.options.update") + if !ok { + t.Fatalf("expected session.options.update in %+v", requests.snapshot()) + } + if arr, isArr := update.Params["includedBuiltinSkills"].([]any); !isArr || len(arr) != 0 { + t.Fatalf("expected includedBuiltinSkills=[], got %#v", update.Params["includedBuiltinSkills"]) + } + }) + + t.Run("explicit built-in skill allowlist is preserved in empty mode", func(t *testing.T) { + client, requests, cleanup := newInMemoryClientWithOptions(t, &ClientOptions{ + Mode: ModeEmpty, + BaseDirectory: "/tmp/copilot-test", + }) + defer cleanup() + + session, err := client.CreateSession(t.Context(), &SessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + AvailableTools: []string{}, + IncludedBuiltinSkills: []string{"code-review"}, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + defer session.Disconnect() + + update, ok := findRequest(requests.snapshot(), "session.options.update") + if !ok { + t.Fatalf("expected session.options.update in %+v", requests.snapshot()) + } + skills, ok := update.Params["includedBuiltinSkills"].([]any) + if !ok || len(skills) != 1 || skills[0] != "code-review" { + t.Fatalf("expected includedBuiltinSkills=[code-review], got %#v", update.Params["includedBuiltinSkills"]) + } + }) + + t.Run("copilot-cli mode does not inject includedBuiltinSkills", func(t *testing.T) { + client, requests, cleanup := newInMemoryClient(t) + defer cleanup() + + session, err := client.CreateSession(t.Context(), &SessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + OnEvent: func(SessionEvent) {}, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + defer session.Disconnect() + + for _, r := range requests.snapshot() { + if _, present := r.Params["includedBuiltinSkills"]; present { + t.Fatalf("did not expect includedBuiltinSkills in %s params %+v", r.Method, r.Params) + } + } + }) +} + type recordedRequest struct { Method string Params map[string]any @@ -2171,13 +2340,18 @@ func (r *requestRecorder) clear() { func newInMemoryClient(t *testing.T) (*Client, *requestRecorder, func()) { t.Helper() + return newInMemoryClientWithOptions(t, &ClientOptions{}) +} + +func newInMemoryClientWithOptions(t *testing.T, opts *ClientOptions) (*Client, *requestRecorder, func()) { + t.Helper() stdinR, stdinW := io.Pipe() stdoutR, stdoutW := io.Pipe() rpcClient := jsonrpc2.NewClient(stdinW, stdoutR) rpcClient.Start() - client := NewClient(&ClientOptions{}) + client := NewClient(opts) client.client = rpcClient client.RPC = rpc.NewServerRPC(rpcClient) client.state = stateConnected @@ -3834,6 +4008,26 @@ func TestSessionRequests_ManagedSettings(t *testing.T) { } }) + t.Run("accepts future bypass-permissions modes", func(t *testing.T) { + req := createSessionRequest{ManagedSettings: &ManagedSettings{ + Permissions: &ManagedSettingsPermissions{ + DisableBypassPermissionsMode: DisableBypassPermissionsMode("future-fail-closed-mode"), + }, + }} + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + perms := m["managedSettings"].(map[string]any)["permissions"].(map[string]any) + if perms["disableBypassPermissionsMode"] != "future-fail-closed-mode" { + t.Errorf("Expected future mode preserved, got %v", perms["disableBypassPermissionsMode"]) + } + }) + t.Run("omits managedSettings when nil", func(t *testing.T) { req := createSessionRequest{} data, _ := json.Marshal(req) diff --git a/go/github_token_provider.go b/go/github_token_provider.go new file mode 100644 index 0000000000..8f1233b7cd --- /dev/null +++ b/go/github_token_provider.go @@ -0,0 +1,84 @@ +package copilot + +import ( + "fmt" + + "github.com/github/copilot-sdk/go/rpc" +) + +// GitHubTokenRequestReason describes why the runtime needs a GitHub token. +// +// Experimental: GitHubTokenRequestReason may change or be removed. +type GitHubTokenRequestReason = rpc.GitHubTokenAcquireReason + +const ( + // GitHubTokenRequestReasonInitial indicates the session needs its initial token. + GitHubTokenRequestReasonInitial = rpc.GitHubTokenAcquireReasonInitial + // GitHubTokenRequestReasonRefresh indicates the session needs a refreshed token. + GitHubTokenRequestReasonRefresh = rpc.GitHubTokenAcquireReasonRefresh +) + +// GitHubTokenProviderArgs contains the context for a GitHub token request. +// +// Experimental: GitHubTokenProviderArgs may change or be removed. +type GitHubTokenProviderArgs struct { + // Host is the effective GitHub host for which a token is needed. + Host string + // SessionID identifies the session receiving the token. It is nil before a + // cloud session has been assigned an ID. + SessionID *string + // Reason indicates whether this is the initial token or a refresh. + Reason GitHubTokenRequestReason +} + +// GitHubToken contains a GitHub access token returned by a provider. +// +// Experimental: GitHubToken may change or be removed. +type GitHubToken struct { + // AccessToken is the GitHub access token. + AccessToken string + // TokenType is the OAuth token type. The runtime defaults it to "bearer". + TokenType *string + // ExpiresIn is the required positive number of seconds remaining when the + // callback completes. Production GitHub tokens typically last eight hours. + ExpiresIn int64 +} + +// String returns a redacted description that never includes the access token. +func (t GitHubToken) String() string { + tokenType := "" + if t.TokenType != nil { + tokenType = *t.TokenType + } + return fmt.Sprintf("GitHubToken{TokenType:%q, ExpiresIn:%d, AccessToken:}", tokenType, t.ExpiresIn) +} + +// GoString returns a redacted Go-syntax description that never includes the access token. +func (t GitHubToken) GoString() string { + return t.String() +} + +// GitHubTokenProviderResult is the result of a GitHub token request. +// +// Experimental: GitHubTokenProviderResult may change or be removed. +type GitHubTokenProviderResult struct { + Cancelled bool + Token *GitHubToken +} + +// GitHubTokenResult returns a successful token-provider result. +func GitHubTokenResult(token *GitHubToken) *GitHubTokenProviderResult { + return &GitHubTokenProviderResult{Token: token} +} + +// GitHubTokenCancelled returns a result indicating that token acquisition was cancelled. +func GitHubTokenCancelled() *GitHubTokenProviderResult { + return &GitHubTokenProviderResult{Cancelled: true} +} + +// GitHubTokenProvider acquires session-scoped GitHub tokens on demand. Initial +// cancellation, errors, and invalid token responses reject session creation or +// resume instead of falling back to ambient authentication. +// +// Experimental: GitHubTokenProvider may change or be removed. +type GitHubTokenProvider func(args GitHubTokenProviderArgs) (*GitHubTokenProviderResult, error) diff --git a/go/github_token_provider_test.go b/go/github_token_provider_test.go new file mode 100644 index 0000000000..f00837379f --- /dev/null +++ b/go/github_token_provider_test.go @@ -0,0 +1,344 @@ +package copilot + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "sync" + "testing" + "time" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" + "github.com/github/copilot-sdk/go/rpc" +) + +func TestGitHubTokenProviderConfigValidation(t *testing.T) { + provider := func(GitHubTokenProviderArgs) (*GitHubTokenProviderResult, error) { + return GitHubTokenCancelled(), nil + } + + if _, err := NewClient(nil).CreateSession(t.Context(), &SessionConfig{ + GitHubToken: "static", + GitHubTokenProvider: provider, + }); err == nil || !strings.Contains(err.Error(), "cannot be used together") { + t.Fatalf("CreateSession error = %v", err) + } + if _, err := NewClient(nil).ResumeSession(t.Context(), "session", &ResumeSessionConfig{ + GitHubToken: "static", + GitHubTokenProvider: provider, + }); err == nil || !strings.Contains(err.Error(), "cannot be used together") { + t.Fatalf("ResumeSession error = %v", err) + } +} + +func TestGitHubTokenProviderCreateRequestAndCallback(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + client.setupNotificationHandler() + + var createParams json.RawMessage + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + createParams = append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil + }) + server.SetRequestHandler("session.destroy", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{}`), nil + }) + + var gotArgs GitHubTokenProviderArgs + session, err := client.CreateSession(t.Context(), &SessionConfig{ + GitHubTokenProvider: func(args GitHubTokenProviderArgs) (*GitHubTokenProviderResult, error) { + gotArgs = args + return GitHubTokenResult(&GitHubToken{ + AccessToken: "secret-token", + TokenType: String("bearer"), + ExpiresIn: 8 * 60 * 60, + }), nil + }, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + var wire struct { + RegistrationID string `json:"gitHubTokenProviderRegistrationId"` + GitHubToken string `json:"gitHubToken"` + } + if err := json.Unmarshal(createParams, &wire); err != nil { + t.Fatal(err) + } + if wire.RegistrationID == "" { + t.Fatal("gitHubTokenProviderRegistrationId was not serialized") + } + if wire.GitHubToken != "" { + t.Fatal("static gitHubToken should not be serialized") + } + + sessionID := session.SessionID + raw, rpcErr := server.Request(t.Context(), "gitHubToken.getToken", &rpc.GitHubTokenAcquireRequest{ + RegistrationID: wire.RegistrationID, + Host: "github.example.com", + SessionID: &sessionID, + Reason: rpc.GitHubTokenAcquireReasonRefresh, + }) + if rpcErr != nil { + t.Fatalf("getToken failed: %v", rpcErr) + } + var tokenResult struct { + Kind string `json:"kind"` + AccessToken string `json:"accessToken"` + ExpiresIn int64 `json:"expiresIn"` + } + if err := json.Unmarshal(raw, &tokenResult); err != nil { + t.Fatal(err) + } + if tokenResult.Kind != "token" || tokenResult.AccessToken != "secret-token" || tokenResult.ExpiresIn != 8*60*60 { + t.Fatalf("unexpected token result: %+v", tokenResult) + } + if gotArgs.Host != "github.example.com" || gotArgs.SessionID == nil || + *gotArgs.SessionID != sessionID || gotArgs.Reason != GitHubTokenRequestReasonRefresh { + t.Fatalf("unexpected callback args: %+v", gotArgs) + } + + if err := session.Disconnect(); err != nil { + t.Fatal(err) + } + if len(client.gitHubTokenProviders) != 0 { + t.Fatal("provider registration was not removed on disconnect") + } + if _, rpcErr := server.Request(t.Context(), "gitHubToken.getToken", &rpc.GitHubTokenAcquireRequest{ + RegistrationID: wire.RegistrationID, + Host: "github.com", + Reason: rpc.GitHubTokenAcquireReasonInitial, + }); rpcErr == nil { + t.Fatal("unknown registration ID should return a handler error") + } + + var resumeParams json.RawMessage + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + resumeParams = append(json.RawMessage(nil), params...) + return []byte(`{"sessionId":"resumed-session","workspacePath":"/workspace"}`), nil + }) + resumed, err := client.ResumeSession(t.Context(), "resumed-session", &ResumeSessionConfig{ + GitHubTokenProvider: func(GitHubTokenProviderArgs) (*GitHubTokenProviderResult, error) { + return GitHubTokenCancelled(), nil + }, + }) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(resumeParams, &wire); err != nil { + t.Fatal(err) + } + if wire.RegistrationID == "" { + t.Fatal("resume did not serialize gitHubTokenProviderRegistrationId") + } + if err := resumed.Disconnect(); err != nil { + t.Fatal(err) + } +} + +func TestGitHubTokenProviderResultsErrorsAndRollback(t *testing.T) { + client := &Client{} + adapter := &gitHubTokenAdapter{client: client} + + cancelID := client.registerGitHubTokenProvider(func(GitHubTokenProviderArgs) (*GitHubTokenProviderResult, error) { + return GitHubTokenCancelled(), nil + }) + result, err := adapter.GetToken(&rpc.GitHubTokenAcquireRequest{RegistrationID: cancelID}) + if err != nil { + t.Fatal(err) + } + if _, ok := result.(*rpc.GitHubTokenAcquireResultCancelled); !ok { + t.Fatalf("result type = %T", result) + } + + sentinel := errors.New("provider failed") + errorID := client.registerGitHubTokenProvider(func(GitHubTokenProviderArgs) (*GitHubTokenProviderResult, error) { + return nil, sentinel + }) + if _, err := adapter.GetToken(&rpc.GitHubTokenAcquireRequest{RegistrationID: errorID}); !errors.Is(err, sentinel) { + t.Fatalf("provider error = %v", err) + } + + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + rollbackClient := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + server.SetRequestHandler("session.create", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return nil, &jsonrpc2.Error{Code: -32000, Message: "create failed"} + }) + if _, err := rollbackClient.CreateSession(t.Context(), &SessionConfig{ + GitHubTokenProvider: func(GitHubTokenProviderArgs) (*GitHubTokenProviderResult, error) { + return GitHubTokenCancelled(), nil + }, + }); err == nil { + t.Fatal("expected create failure") + } + if len(rollbackClient.gitHubTokenProviders) != 0 { + t.Fatal("provider registration was not rolled back") + } +} + +func TestGitHubTokenStringRedactsAccessToken(t *testing.T) { + token := GitHubToken{AccessToken: "secret-token", ExpiresIn: 28_800} + + if got := fmt.Sprintf("%v %#v", token, token); strings.Contains(got, token.AccessToken) { + t.Fatalf("GitHubToken formatting exposed the access token: %s", got) + } +} + +func TestGitHubTokenProviderCleanupOnDisconnectError(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + server.SetRequestHandler("session.destroy", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return nil, &jsonrpc2.Error{Code: -32000, Message: "destroy failed"} + }) + client := &Client{} + registrationID := client.registerGitHubTokenProvider(func(GitHubTokenProviderArgs) (*GitHubTokenProviderResult, error) { + return GitHubTokenCancelled(), nil + }) + session := newSession("cleanup-session", rpcClient, "", false) + session.setGitHubTokenProviderRegistrationRelease(func() { + client.unregisterGitHubTokenProvider(registrationID) + }) + + if err := session.Disconnect(); err == nil || !strings.Contains(err.Error(), "destroy failed") { + t.Fatalf("Disconnect error = %v", err) + } + if len(client.gitHubTokenProviders) != 0 { + t.Fatal("provider registration was not removed after disconnect failed") + } +} + +func TestGitHubTokenProviderReleaseBeforeOwnershipTransfer(t *testing.T) { + client := &Client{} + registrationID := client.registerGitHubTokenProvider(func(GitHubTokenProviderArgs) (*GitHubTokenProviderResult, error) { + return GitHubTokenCancelled(), nil + }) + session := &Session{} + + session.releaseGitHubTokenProviderRegistration() + session.setGitHubTokenProviderRegistrationRelease(func() { + client.unregisterGitHubTokenProvider(registrationID) + }) + + if len(client.gitHubTokenProviders) != 0 { + t.Fatal("provider registration was not removed after a pending session had already been retired") + } +} + +func TestGitHubTokenProviderCleanupOnDelete(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + server.SetRequestHandler("session.delete", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{"success":true}`), nil + }) + client := &Client{ + client: rpcClient, + sessions: make(map[string]*Session), + state: stateConnected, + } + registrationID := client.registerGitHubTokenProvider(func(GitHubTokenProviderArgs) (*GitHubTokenProviderResult, error) { + return GitHubTokenCancelled(), nil + }) + session := newSession("delete-session", rpcClient, "", false) + session.setGitHubTokenProviderRegistrationRelease(func() { + client.unregisterGitHubTokenProvider(registrationID) + }) + client.sessions[session.SessionID] = session + + if err := client.DeleteSession(t.Context(), session.SessionID); err != nil { + t.Fatal(err) + } + if len(client.gitHubTokenProviders) != 0 { + t.Fatal("provider registration was not removed after session deletion") + } +} + +func TestSessionOperationsSerializeBySessionID(t *testing.T) { + client := &Client{} + unlockFirst := client.lockSessionOperation("same-session") + sameSessionAcquired := make(chan struct{}) + go func() { + unlock := client.lockSessionOperation("same-session") + close(sameSessionAcquired) + unlock() + }() + + select { + case <-sameSessionAcquired: + t.Fatal("same-session operation was not serialized") + case <-time.After(25 * time.Millisecond): + } + + otherSessionAcquired := make(chan struct{}) + go func() { + unlock := client.lockSessionOperation("other-session") + close(otherSessionAcquired) + unlock() + }() + select { + case <-otherSessionAcquired: + case <-time.After(time.Second): + t.Fatal("different-session operation was unnecessarily blocked") + } + + unlockFirst() + select { + case <-sameSessionAcquired: + case <-time.After(time.Second): + t.Fatal("same-session operation did not proceed after release") + } +} + +func TestGitHubTokenProvidersClearedOnConnectionClose(t *testing.T) { + client := &Client{state: stateConnected} + client.registerGitHubTokenProvider(func(GitHubTokenProviderArgs) (*GitHubTokenProviderResult, error) { + return GitHubTokenCancelled(), nil + }) + + client.handleConnectionClose() + + if len(client.gitHubTokenProviders) != 0 { + t.Fatal("provider registrations were not cleared after connection closure") + } +} + +func TestGitHubTokenProviderConcurrentRegistrationsAreIsolated(t *testing.T) { + client := &Client{} + adapter := &gitHubTokenAdapter{client: client} + idA := client.registerGitHubTokenProvider(func(GitHubTokenProviderArgs) (*GitHubTokenProviderResult, error) { + return GitHubTokenResult(&GitHubToken{AccessToken: "a", ExpiresIn: 1}), nil + }) + idB := client.registerGitHubTokenProvider(func(GitHubTokenProviderArgs) (*GitHubTokenProviderResult, error) { + return GitHubTokenResult(&GitHubToken{AccessToken: "b", ExpiresIn: 1}), nil + }) + + var wg sync.WaitGroup + for id, want := range map[string]string{idA: "a", idB: "b"} { + wg.Add(1) + go func() { + defer wg.Done() + result, err := adapter.GetToken(&rpc.GitHubTokenAcquireRequest{RegistrationID: id}) + if err != nil { + t.Error(err) + return + } + if got := result.(*rpc.GitHubTokenAcquireResultToken).AccessToken; got != want { + t.Errorf("token = %q, want %q", got, want) + } + }() + } + wg.Wait() +} diff --git a/go/internal/e2e/rpc_tasks_and_handlers_e2e_test.go b/go/internal/e2e/rpc_tasks_and_handlers_e2e_test.go index 0267f8d042..648855e5a3 100644 --- a/go/internal/e2e/rpc_tasks_and_handlers_e2e_test.go +++ b/go/internal/e2e/rpc_tasks_and_handlers_e2e_test.go @@ -127,6 +127,7 @@ func TestRPCTasksAndHandlersE2E(t *testing.T) { }) t.Run("should report implemented error for invalid task agent model", func(t *testing.T) { + ctx.ConfigureForTest(t) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) diff --git a/go/mode_empty.go b/go/mode_empty.go index 6057b2661f..6e238c58c4 100644 --- a/go/mode_empty.go +++ b/go/mode_empty.go @@ -225,7 +225,8 @@ func (c *Client) applyResumeDefaultsForMode(config *ResumeSessionConfig) { // updateSessionOptionsForMode applies the per-mode safe-defaults patch via // session.options.update after create/resume succeeds. In empty mode the // four overridable feature flags default to safe values; caller values win. -// installedPlugins=[] is unconditional in empty mode. +// installedPlugins=[] is unconditional in empty mode. IncludedBuiltinSkills +// defaults to [] but callers can explicitly allow selected runtime-bundled skills. func (c *Client) updateSessionOptionsForMode(ctx context.Context, session *Session, base optBackInFields) error { patch := &rpc.SessionUpdateOptionsParams{} hasAny := false @@ -255,6 +256,11 @@ func (c *Client) updateSessionOptionsForMode(ctx context.Context, session *Sessi patch.ManageScheduleEnabled = &f } patch.InstalledPlugins = []rpc.SessionInstalledPlugin{} + if base.IncludedBuiltinSkills != nil { + patch.IncludedBuiltinSkills = base.IncludedBuiltinSkills + } else { + patch.IncludedBuiltinSkills = []string{} + } hasAny = true } else { if base.SkipCustomInstructions != nil { @@ -273,6 +279,10 @@ func (c *Client) updateSessionOptionsForMode(ctx context.Context, session *Sessi patch.ManageScheduleEnabled = base.ManageScheduleEnabled hasAny = true } + if base.IncludedBuiltinSkills != nil { + patch.IncludedBuiltinSkills = base.IncludedBuiltinSkills + hasAny = true + } } if !hasAny { return nil @@ -297,4 +307,5 @@ type optBackInFields struct { CustomAgentsLocalOnly *bool CoauthorEnabled *bool ManageScheduleEnabled *bool + IncludedBuiltinSkills []string } diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 69b44cef4c..2b3943988d 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -866,6 +866,9 @@ type AuthIdentity struct { Host string `json:"host"` // Authenticated login, when available Login *string `json:"login,omitempty"` + // Opaque SDK GitHub credential registration backing this identity. Routing metadata only; + // never a credential. + RegistrationID *string `json:"registrationId,omitempty"` // Authentication type Type AuthInfoType `json:"type"` } @@ -999,6 +1002,8 @@ type TokenAuthInfo struct { CopilotUser *CopilotUserResponse `json:"copilotUser,omitempty"` // Authentication host. Host string `json:"host"` + // Opaque native GitHub credential registration backing this token identity, when applicable. + RegistrationID *string `json:"registrationId,omitempty"` // The token value itself. Treat as a secret. Token string `json:"token"` } @@ -1008,6 +1013,24 @@ func (TokenAuthInfo) Type() AuthInfoType { return AuthInfoTypeToken } +// Authentication-info variant backed by an SDK GitHub token callback. It carries routing +// metadata but never a plaintext token. +// Experimental: TokenProviderAuthInfo is part of an experimental API and may change or be +// removed. +type TokenProviderAuthInfo struct { + // Snapshot of the authenticated user's Copilot subscription info, if known. + CopilotUser *CopilotUserResponse `json:"copilotUser,omitempty"` + // Authentication host. + Host string `json:"host"` + // Opaque SDK callback registration identifier. + RegistrationID string `json:"registrationId"` +} + +func (TokenProviderAuthInfo) authInfo() {} +func (TokenProviderAuthInfo) Type() AuthInfoType { + return AuthInfoTypeTokenProvider +} + // Authentication-info variant for OAuth user auth, with host and login; the token remains // in the runtime secret store. // Experimental: UserAuthInfo is part of an experimental API and may change or be removed. @@ -1949,6 +1972,25 @@ type ConfigureSessionExtensionsParams struct { SessionID string `json:"sessionId"` } +// 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: ConnectClientInfo is part of an experimental API and may change or be +// removed. +// Internal: ConnectClientInfo is an internal SDK API and is not part of the public surface. +type ConnectClientInfo struct { + // Name of the host editor, e.g. `"vscode"`. + EditorName *string `json:"editorName,omitempty"` + // Version of the host editor, e.g. `"1.124.2"`. Ignored unless it looks like a version + // string. + EditorVersion *string `json:"editorVersion,omitempty"` + // Name of the Copilot extension within the host, e.g. `"copilot-chat"`. + ExtensionName *string `json:"extensionName,omitempty"` + // Version of the Copilot extension within the host, e.g. `"0.54.0"`. Ignored unless it + // looks like a version string. + ExtensionVersion *string `json:"extensionVersion,omitempty"` +} + // Metadata for a connected remote session. // Experimental: ConnectedRemoteSessionMetadata is part of an experimental API and may // change or be removed. @@ -2002,6 +2044,10 @@ type ConnectRemoteSessionParams struct { // Experimental: ConnectRequest is part of an experimental API and may change or be removed. // Internal: ConnectRequest is an internal SDK API and is not part of the public surface. type ConnectRequest struct { + // Identity of the integrating host. Optional; omit it to keep the default attribution. + // Internal: ClientInfo is part of the SDK's internal API surface and is not intended for + // external use. + ClientInfo *ConnectClientInfo `json:"clientInfo,omitempty"` // 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. @@ -3700,6 +3746,10 @@ type GitHubTelemetryClientInfo struct { CLIVersion string `json:"cli_version"` // Copilot subscription plan, when known. CopilotPlan *string `json:"copilot_plan,omitempty"` + // Number of logical CPU cores on the host. + CpuCount *int64 `json:"cpu_count,omitempty"` + // Distinct CPU model names for the host, comma-separated. + CpuModel *string `json:"cpu_model,omitempty"` // Stable machine identifier for the device. DevDeviceID *string `json:"dev_device_id,omitempty"` // Whether the user is a GitHub/Microsoft staff member. @@ -3765,6 +3815,61 @@ type GitHubTelemetryNotification struct { SessionID *string `json:"sessionId,omitempty"` } +// Asks the SDK client to acquire a GitHub access token from an opaque callback registration. +// Experimental: GitHubTokenAcquireRequest is part of an experimental API and may change or +// be removed. +type GitHubTokenAcquireRequest struct { + // Effective GitHub host for which the callback must return a token. + Host string `json:"host"` + // Why the runtime is requesting a GitHub credential. + Reason GitHubTokenAcquireReason `json:"reason"` + // Opaque identifier generated by the SDK for this callback registration. + RegistrationID string `json:"registrationId"` + // Session receiving the token. Absent only before a cloud session has been assigned its id. + SessionID *string `json:"sessionId,omitempty"` +} + +// SDK host response to a GitHub credential request. +// Experimental: GitHubTokenAcquireResult is part of an experimental API and may change or +// be removed. +type GitHubTokenAcquireResult interface { + githubTokenAcquireResult() + Kind() GitHubTokenAcquireResultKind +} + +type RawGitHubTokenAcquireResultData struct { + Discriminator GitHubTokenAcquireResultKind + Raw json.RawMessage +} + +func (RawGitHubTokenAcquireResultData) githubTokenAcquireResult() {} +func (r RawGitHubTokenAcquireResultData) Kind() GitHubTokenAcquireResultKind { + return r.Discriminator +} + +type GitHubTokenAcquireResultCancelled struct { +} + +func (GitHubTokenAcquireResultCancelled) githubTokenAcquireResult() {} +func (GitHubTokenAcquireResultCancelled) Kind() GitHubTokenAcquireResultKind { + return GitHubTokenAcquireResultKindCancelled +} + +type GitHubTokenAcquireResultToken struct { + // GitHub access token acquired by the SDK host. + AccessToken string `json:"accessToken"` + // Remaining token lifetime in seconds when callback execution completes. It must exceed the + // one-hour preflight refresh threshold. + ExpiresIn int64 `json:"expiresIn"` + // OAuth token type. Defaults to bearer when omitted. + TokenType *string `json:"tokenType,omitempty"` +} + +func (GitHubTokenAcquireResultToken) githubTokenAcquireResult() {} +func (GitHubTokenAcquireResultToken) Kind() GitHubTokenAcquireResultKind { + return GitHubTokenAcquireResultKindToken +} + // Pending external tool call request ID, with the tool result or an error describing why it // failed. // Experimental: HandlePendingToolCallRequest is part of an experimental API and may change @@ -4074,6 +4179,12 @@ type InstalledPlugin struct { Enabled bool `json:"enabled"` // Installation timestamp InstalledAt string `json:"installed_at"` + // 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. + InstalledFrom *string `json:"installed_from,omitempty"` // Marketplace the plugin came from (empty string for direct repo installs) Marketplace string `json:"marketplace"` // Plugin name @@ -4100,6 +4211,13 @@ type InstalledPluginInfo struct { DirectSourceID *string `json:"directSourceId,omitempty"` // Whether the plugin is currently enabled for new sessions Enabled bool `json:"enabled"` + // 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". + InstalledFrom *string `json:"installedFrom,omitempty"` // Marketplace the plugin came from. Empty string ("") for direct repo / URL / local // installs. Marketplace string `json:"marketplace"` @@ -5257,7 +5375,7 @@ type MCPOauthPendingRequestResponseToken struct { AccessToken string `json:"accessToken"` // Token lifetime in seconds, if known. ExpiresIn *int64 `json:"expiresIn,omitempty"` - // OAuth token type. Defaults to Bearer when omitted. + // OAuth token type. Defaults to bearer when omitted. TokenType *string `json:"tokenType,omitempty"` } @@ -6669,6 +6787,10 @@ type Model struct { DefaultReasoningEffort *string `json:"defaultReasoningEffort,omitempty"` // Model identifier (e.g., "claude-sonnet-4.5") ID string `json:"id"` + // 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. + InfoMessages []ModelMessage `json:"infoMessages,omitzero"` // Model capability category for grouping in the model picker ModelPickerCategory *ModelPickerCategory `json:"modelPickerCategory,omitempty"` // Relative cost tier for token-based billing users @@ -6684,6 +6806,13 @@ type Model struct { SupportedContextTiers []string `json:"supportedContextTiers,omitzero"` // Supported reasoning effort levels (only present if model supports reasoning effort) SupportedReasoningEfforts []string `json:"supportedReasoningEfforts,omitzero"` + // 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. + WarningMessages []ModelMessage `json:"warningMessages,omitzero"` + // Warning text the service requires hosts to surface for this model. Present only when the + // service published at least one warning. + WarningText *ModelWarningText `json:"warningText,omitempty"` } // Managed, repository, and CLI model overrides to overlay onto the session at startup. @@ -6906,6 +7035,18 @@ type ModelListRequest struct { SkipCache *bool `json:"skipCache,omitempty"` } +// A service-published message about a model, carrying a stable machine-readable code +// alongside human-readable text. +// Experimental: ModelMessage is part of an experimental API and may change or be removed. +type ModelMessage struct { + // 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`. + Code string `json:"code"` + // Human-readable message text intended for display to the user. + Message string `json:"message"` +} + // Experimental: ModelPickerPersistenceRequest is part of an experimental API and may change // or be removed. type ModelPickerPersistenceRequest struct { @@ -7050,6 +7191,15 @@ type ModelSwitchToResult struct { Warning *string `json:"warning,omitempty"` } +// Service-published warning text that hosts should display when presenting a model. +// Experimental: ModelWarningText is part of an experimental API and may change or be +// removed. +type ModelWarningText struct { + // Data-retention warning for the model. The text may contain Markdown links and should be + // rendered as Markdown when supported. + DataRetention *string `json:"dataRetention,omitempty"` +} + // Agent interaction mode to apply to the session. // Experimental: ModeSetRequest is part of an experimental API and may change or be removed. type ModeSetRequest struct { @@ -7848,6 +7998,9 @@ func (PermissionDecisionApproveForSessionApprovalWrite) Kind() PermissionDecisio type PermissionDecisionContext struct { // Disposition of the permission request as observed by the responding client. Outcome PermissionDecisionOutcome `json:"outcome"` + // 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. + ResponseCapability *PermissionResponseCapability `json:"responseCapability,omitempty"` // Controlled reason or actor responsible for the response. Source PermissionDecisionSource `json:"source"` // Client surface that submitted the response. @@ -7926,7 +8079,9 @@ type PermissionLocationResolveResult struct { // be removed. type PermissionPathsAddParams struct { // Directory to add to the allow-list. The runtime resolves and validates the path before - // adding. + // 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. Path string `json:"path"` } @@ -7953,9 +8108,11 @@ type PermissionPathsAllowedCheckResult struct { // removed. type PermissionPathsConfig struct { // 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). + // 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). AdditionalDirectories []string `json:"additionalDirectories,omitzero"` // Whether to include the system temp directory in the allowed list (defaults to true). // Ignored when `unrestricted` is true. @@ -9589,6 +9746,10 @@ type QueuePendingItems struct { // Experimental: QueuePendingItemsResult is part of an experimental API and may change or be // removed. type QueuePendingItemsResult struct { + // 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. + InFlightSteeringCount *int64 `json:"inFlightSteeringCount,omitempty"` // Pending queued items in submission order. Includes user messages, queued slash commands, // and queued model changes; omits internal system items. Items []QueuePendingItems `json:"items"` @@ -10002,9 +10163,9 @@ type SandboxConfig struct { // 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 + // 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 — @@ -10533,6 +10694,9 @@ type SessionAuthInfoResult struct { Host string `json:"host"` // Authenticated login, when available Login *string `json:"login,omitempty"` + // Opaque SDK GitHub credential registration backing this identity. Routing metadata only; + // never a credential. + RegistrationID *string `json:"registrationId,omitempty"` // Authentication type Type AuthInfoType `json:"type"` } @@ -11187,6 +11351,12 @@ type SessionInstalledPlugin struct { Enabled bool `json:"enabled"` // Installation timestamp (ISO-8601) InstalledAt string `json:"installed_at"` + // 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. + InstalledFrom *string `json:"installed_from,omitempty"` // Marketplace the plugin came from (empty string for direct repo installs) Marketplace string `json:"marketplace"` // Plugin name @@ -11474,8 +11644,12 @@ type SessionManagedPermissions struct { Ask []string `json:"ask,omitzero"` // Permission rules that block matching operations. Deny has highest precedence. Deny []string `json:"deny,omitzero"` - // When set to `disable`, prevents bypass/allow-all permission modes. - DisableBypassPermissionsMode *DisableBypassPermissionsMode `json:"disableBypassPermissionsMode,omitempty"` + // When set to `disable`, prevents bypass/allow-all permission modes. `allow-auto-only` + // blocks full allow-all but permits advisory auto-approval. Any other value is accepted + // rather than failing the session, but is enforced as `disable`: the key is only present to + // restrict something, so a mode this runtime cannot interpret fails closed to the most + // restrictive one it knows. Omit the key entirely to impose no restriction. + DisableBypassPermissionsMode *string `json:"disableBypassPermissionsMode,omitempty"` } // Managed settings an SDK host may inject at session startup. Only permissions are accepted @@ -11636,11 +11810,15 @@ type SessionOpenOptions struct { AdditionalContentExclusionPolicies []SessionOpenOptionsAdditionalContentExclusionPolicy `json:"additionalContentExclusionPolicies,omitzero"` // Additional directories the agent may access beyond the working directory. Each entry is // granted to the session's file-access allow-list and surfaced to the model (system prompt - // context and `@`-mention completion). Absolute paths are recommended; a relative path is - // resolved against the session's working directory. Nonexistent or unresolvable entries are - // skipped with a warning. This is applied on both session creation and resume, and is not - // persisted: a resumed session that omits this option does not retain previously supplied - // directories (re-supply them, exactly as the CLI re-passes `--add-dir`). + // context and `@`-mention completion). Conventional `.github/skills/` and `.github/agents/` + // definitions under each directory also join the session's project catalogs when their + // existing subsystem gates are enabled: added-root skills require both + // `enableConfigDiscovery` and effective `enableSkills`; added-root agents require + // `enableConfigDiscovery`. Supplying a directory therefore activates configuration from it + // and should be treated as a trust decision. Absolute paths are recommended; a relative + // path is resolved against the session's working directory. Nonexistent or unresolvable + // entries are skipped with a warning. This is applied during session creation and cold + // resume and is not persisted, so a cold resume must re-supply the directories. AdditionalDirectories []string `json:"additionalDirectories,omitzero"` // Runtime context discriminator for agent filtering. AgentContext *string `json:"agentContext,omitempty"` @@ -11739,6 +11917,10 @@ type SessionOpenOptions struct { // are available, subject to runtime availability and exclusions. Custom agents with the // same name remain available. IncludedBuiltinAgents []string `json:"includedBuiltinAgents,omitzero"` + // 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. + IncludedBuiltinSkills []string `json:"includedBuiltinSkills,omitzero"` // Installed plugins visible to the session. InstalledPlugins []InstalledPlugin `json:"installedPlugins,omitzero"` // Stable integration identifier for analytics. @@ -11789,6 +11971,11 @@ type SessionOpenOptions struct { RunningInInteractiveMode *bool `json:"runningInInteractiveMode,omitempty"` // Resolved sandbox configuration. SandboxConfig *SandboxConfig `json:"sandboxConfig,omitempty"` + // Origin of the sandbox choice. The runtime uses this only for internal telemetry + // provenance; managed policy is derived independently. + // Internal: SandboxConfigSource is part of the SDK's internal API surface and is not + // intended for external use. + SandboxConfigSource *SandboxConfigSource `json:"sandboxConfigSource,omitempty"` // Capabilities enabled for this session. SessionCapabilities []SessionCapability `json:"sessionCapabilities,omitzero"` // Optional stable session identifier to use for a new session. @@ -12219,7 +12406,7 @@ type SessionSetCredentialsParams struct { // 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. - Credentials AuthInfo `json:"credentials,omitempty"` + Credentials SettableAuthInfo `json:"credentials,omitempty"` } // Indicates whether the credential update succeeded. @@ -12837,6 +13024,10 @@ type SessionUpdateOptionsParams struct { // are available, subject to runtime availability and exclusions. Custom agents with the // same name remain available. Set to null to remove the allowlist restriction. IncludedBuiltinAgents []string `json:"includedBuiltinAgents,omitzero"` + // 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. + IncludedBuiltinSkills []string `json:"includedBuiltinSkills,omitzero"` // Full set of installed plugins for the session. Replaces the existing list; the runtime // invalidates the skills cache only when the list materially changes. InstalledPlugins []SessionInstalledPlugin `json:"installedPlugins,omitzero"` @@ -12875,6 +13066,11 @@ type SessionUpdateOptionsParams struct { RunningInInteractiveMode *bool `json:"runningInInteractiveMode,omitempty"` // Resolved sandbox configuration. SandboxConfig *SandboxConfig `json:"sandboxConfig,omitempty"` + // Origin of the sandbox choice. The runtime uses this only for internal telemetry + // provenance; managed policy is derived independently. + // Internal: SandboxConfigSource is part of the SDK's internal API surface and is not + // intended for external use. + SandboxConfigSource *SandboxConfigSource `json:"sandboxConfigSource,omitempty"` // 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. @@ -12948,6 +13144,68 @@ type SessionWorkingDirectoryContext struct { type SessionWorkspacesCreateFileResult struct { } +// Authentication credentials accepted by session.gitHubAuth.setCredentials. Session-owned +// token-provider identities cannot be installed through this method. +// Experimental: SettableAuthInfo is part of an experimental API and may change or be +// removed. +type SettableAuthInfo interface { + settableAuthInfo() + settableAuthInfoType() SettableAuthInfoType +} + +type RawSettableAuthInfoData struct { + Discriminator SettableAuthInfoType + Raw json.RawMessage +} + +func (RawSettableAuthInfoData) settableAuthInfo() {} +func (r RawSettableAuthInfoData) settableAuthInfoType() SettableAuthInfoType { + return r.Discriminator +} +func (APIKeyAuthInfo) settableAuthInfo() {} +func (APIKeyAuthInfo) settableAuthInfoType() SettableAuthInfoType { + return SettableAuthInfoTypeAPIKey +} +func (CopilotAPITokenAuthInfo) settableAuthInfo() {} +func (CopilotAPITokenAuthInfo) settableAuthInfoType() SettableAuthInfoType { + return SettableAuthInfoTypeCopilotAPIToken +} +func (EnvAuthInfo) settableAuthInfo() {} +func (EnvAuthInfo) settableAuthInfoType() SettableAuthInfoType { + return SettableAuthInfoTypeEnv +} +func (GhCLIAuthInfo) settableAuthInfo() {} +func (GhCLIAuthInfo) settableAuthInfoType() SettableAuthInfoType { + return SettableAuthInfoTypeGhCLI +} +func (HMACAuthInfo) settableAuthInfo() {} +func (HMACAuthInfo) settableAuthInfoType() SettableAuthInfoType { + return SettableAuthInfoTypeHMAC +} + +// Token authentication accepted by session.gitHubAuth.setCredentials. +// Experimental: SettableTokenAuthInfo is part of an experimental API and may change or be +// removed. +type SettableTokenAuthInfo struct { + // 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. + CopilotUser *CopilotUserResponse `json:"copilotUser,omitempty"` + // Authentication host. + Host string `json:"host"` + // The token value itself. Treat as a secret. + Token string `json:"token"` +} + +func (SettableTokenAuthInfo) settableAuthInfo() {} +func (SettableTokenAuthInfo) settableAuthInfoType() SettableAuthInfoType { + return SettableAuthInfoTypeToken +} +func (UserAuthInfo) settableAuthInfo() {} +func (UserAuthInfo) settableAuthInfoType() SettableAuthInfoType { + return SettableAuthInfoTypeUser +} + // User-requested shell execution cancellation handle. // Experimental: ShellCancelUserRequestedRequest is part of an experimental API and may // change or be removed. @@ -15551,6 +15809,7 @@ const ( AuthInfoTypeGhCLI AuthInfoType = "gh-cli" AuthInfoTypeHMAC AuthInfoType = "hmac" AuthInfoTypeToken AuthInfoType = "token" + AuthInfoTypeTokenProvider AuthInfoType = "token-provider" AuthInfoTypeUser AuthInfoType = "user" ) @@ -16018,14 +16277,6 @@ const ( DebugCollectLogsSourceShellLog DebugCollectLogsSource = "shell-log" ) -// Experimental: DisableBypassPermissionsMode is part of an experimental API and may change -// or be removed. -type DisableBypassPermissionsMode string - -const ( - DisableBypassPermissionsModeDisable DisableBypassPermissionsMode = "disable" -) - // Effective extension loading and agent-management mode // Experimental: DiscoveredExtensionMode is part of an experimental API and may change or be // removed. @@ -16296,6 +16547,28 @@ const ( FactoryRunStatusRunning FactoryRunStatus = "running" ) +// Why the runtime is requesting a GitHub credential. +// Experimental: GitHubTokenAcquireReason is part of an experimental API and may change or +// be removed. +type GitHubTokenAcquireReason string + +const ( + // The runtime is acquiring the registration's first credential. + GitHubTokenAcquireReasonInitial GitHubTokenAcquireReason = "initial" + // The runtime is replacing a credential that is approaching expiry. + GitHubTokenAcquireReasonRefresh GitHubTokenAcquireReason = "refresh" +) + +// Kind discriminator for GitHubTokenAcquireResult. +// Experimental: GitHubTokenAcquireResultKind is part of an experimental API and may change +// or be removed. +type GitHubTokenAcquireResultKind string + +const ( + GitHubTokenAcquireResultKindCancelled GitHubTokenAcquireResultKind = "cancelled" + GitHubTokenAcquireResultKindToken GitHubTokenAcquireResultKind = "token" +) + // What initiated this compaction request, recorded as the `trigger` on the persisted // `session.compaction_start` / `session.compaction_complete` events. When absent, the // compaction is persisted without trigger attribution (initiator unknown). @@ -16692,6 +16965,8 @@ const ( ) // Kind discriminator for MCPOauthPendingRequestResponse. +// Experimental: MCPOauthPendingRequestResponseKind is part of an experimental API and may +// change or be removed. type MCPOauthPendingRequestResponseKind string const ( @@ -17407,6 +17682,8 @@ const ( type PermissionDecisionSurface string const ( + // An Agent Client Protocol host. + PermissionDecisionSurfaceAcp PermissionDecisionSurface = "acp" // The Copilot App client. PermissionDecisionSurfaceCopilotApp PermissionDecisionSurface = "copilot_app" // The non-interactive Copilot CLI prompt mode. @@ -17462,6 +17739,20 @@ const ( PermissionModeSourceUserSetting PermissionModeSource = "user_setting" ) +// Response capability available to the client when it settled a permission request. +// Experimental: PermissionResponseCapability is part of an experimental API and may change +// or be removed. +type PermissionResponseCapability string + +const ( + // The client could return an automated response but could not ask a user. + PermissionResponseCapabilityHeadless PermissionResponseCapability = "headless" + // The client could ask a user for this decision. + PermissionResponseCapabilityInteractive PermissionResponseCapability = "interactive" + // The client had no response path available. + PermissionResponseCapabilityNone PermissionResponseCapability = "none" +) + // Allowed values for the `PermissionsConfigureAdditionalContentExclusionPolicyScope` // enumeration. // Experimental: PermissionsConfigureAdditionalContentExclusionPolicyScope is part of an @@ -17729,6 +18020,28 @@ const ( RemoteSessionModeOn RemoteSessionMode = "on" ) +// Origin of the sandbox choice supplied by an internal client. +// Experimental: SandboxConfigSource is part of an experimental API and may change or be +// removed. +type SandboxConfigSource string + +const ( + // The client applied the default because no sandbox preference was configured. + SandboxConfigSourceNeverConfigured SandboxConfigSource = "never_configured" + // A repository policy selected the sandbox state. + SandboxConfigSourceRepositoryPolicy SandboxConfigSource = "repository_policy" + // The user disabled the sandbox for the current session. + SandboxConfigSourceSessionDisabled SandboxConfigSource = "session_disabled" + // A command-line flag selected the sandbox state for this session. + SandboxConfigSourceSessionFlag SandboxConfigSource = "session_flag" + // The client disabled the sandbox because the host cannot enforce it. + SandboxConfigSourceUnsupportedHost SandboxConfigSource = "unsupported_host" + // The user's persisted settings disabled the sandbox. + SandboxConfigSourceUserDisabled SandboxConfigSource = "user_disabled" + // The user's persisted settings enabled the sandbox. + SandboxConfigSourceUserEnabled SandboxConfigSource = "user_enabled" +) + // The UI mode the agent was in when this message was sent. Defaults to the session's // current mode. // Experimental: SendAgentMode is part of an experimental API and may change or be removed. @@ -18190,6 +18503,19 @@ const ( SessionWorkingDirectoryContextHostTypeGitHub SessionWorkingDirectoryContextHostType = "github" ) +// Type discriminator for SettableAuthInfo. +type SettableAuthInfoType string + +const ( + SettableAuthInfoTypeAPIKey SettableAuthInfoType = "api-key" + SettableAuthInfoTypeCopilotAPIToken SettableAuthInfoType = "copilot-api-token" + SettableAuthInfoTypeEnv SettableAuthInfoType = "env" + SettableAuthInfoTypeGhCLI SettableAuthInfoType = "gh-cli" + SettableAuthInfoTypeHMAC SettableAuthInfoType = "hmac" + SettableAuthInfoTypeToken SettableAuthInfoType = "token" + SettableAuthInfoTypeUser SettableAuthInfoType = "user" +) + // Controls automatic non-interactive profile loading where supported. Explicit initScripts // are unaffected. // Experimental: ShellInitProfile is part of an experimental API and may change or be @@ -23456,6 +23782,9 @@ func (a *OptionsAPI) Update(ctx context.Context, params *SessionUpdateOptionsPar if params.IncludedBuiltinAgents != nil { req["includedBuiltinAgents"] = params.IncludedBuiltinAgents } + if params.IncludedBuiltinSkills != nil { + req["includedBuiltinSkills"] = params.IncludedBuiltinSkills + } if params.InstalledPlugins != nil { req["installedPlugins"] = params.InstalledPlugins } @@ -23501,6 +23830,9 @@ func (a *OptionsAPI) Update(ctx context.Context, params *SessionUpdateOptionsPar if params.SandboxConfig != nil { req["sandboxConfig"] = *params.SandboxConfig } + if params.SandboxConfigSource != nil { + req["sandboxConfigSource"] = *params.SandboxConfigSource + } if params.SessionCapabilities != nil { req["sessionCapabilities"] = params.SessionCapabilities } @@ -23973,7 +24305,8 @@ func (s *PermissionsAPI) Locations() *PermissionsLocationsAPI { // removed. type PermissionsPathsAPI sessionAPI -// Adds a directory to the session's allow-list. +// Adds a directory to the session's allow-list and activates conventional skill and agent +// definitions under it. // // RPC method: session.permissions.paths.add. // @@ -28107,6 +28440,21 @@ type GitHubTelemetryHandler interface { Event(request *GitHubTelemetryNotification) error } +// Experimental: GitHubTokenHandler contains experimental APIs that may change or be removed. +type GitHubTokenHandler interface { + // GetToken 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. + // + // RPC method: gitHubToken.getToken. + // + // Parameters: Asks the SDK client to acquire a GitHub access token from an opaque callback + // registration. + // + // Returns: SDK host response to a GitHub credential request. + GetToken(request *GitHubTokenAcquireRequest) (GitHubTokenAcquireResult, error) +} + // Experimental: HooksHandler contains experimental APIs that may change or be removed. type HooksHandler interface { // Invoke dispatches one SDK callback hook from the runtime to the connection that @@ -28159,6 +28507,7 @@ type LlmInferenceHandler interface { type ClientGlobalAPIHandlers struct { ExtensionLaunchProvider ExtensionLaunchProviderHandler GitHubTelemetry GitHubTelemetryHandler + GitHubToken GitHubTokenHandler Hooks HooksHandler LlmInference LlmInferenceHandler } @@ -28208,6 +28557,24 @@ func RegisterClientGlobalAPIHandlers(client *jsonrpc2.Client, handlers *ClientGl } return nil, nil }) + client.SetRequestHandler("gitHubToken.getToken", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request GitHubTokenAcquireRequest + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} + } + if handlers == nil || handlers.GitHubToken == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: "No gitHubToken client-global handler registered"} + } + result, err := handlers.GitHubToken.GetToken(&request) + if err != nil { + return nil, clientGlobalHandlerError(err) + } + raw, err := json.Marshal(result) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil + }) client.SetRequestHandler("hooks.invoke", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { var request HookInvokeRequest if err := json.Unmarshal(params, &request); err != nil { diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index fadbc5da1b..78788660f5 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -57,6 +57,12 @@ func unmarshalAuthInfo(data []byte) (AuthInfo, error) { return nil, err } return &d, nil + case AuthInfoTypeTokenProvider: + var d TokenProviderAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case AuthInfoTypeUser: var d UserAuthInfo if err := json.Unmarshal(data, &d); err != nil { @@ -145,6 +151,17 @@ func (r TokenAuthInfo) MarshalJSON() ([]byte, error) { }) } +func (r TokenProviderAuthInfo) MarshalJSON() ([]byte, error) { + type alias TokenProviderAuthInfo + return json.Marshal(struct { + Type AuthInfoType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + func (r UserAuthInfo) MarshalJSON() ([]byte, error) { type alias UserAuthInfo return json.Marshal(struct { @@ -1728,6 +1745,69 @@ func unmarshalFilterMapping(data []byte) (FilterMapping, error) { return nil, errors.New("data did not match any union variant for FilterMapping") } +func unmarshalGitHubTokenAcquireResult(data []byte) (GitHubTokenAcquireResult, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind GitHubTokenAcquireResultKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case GitHubTokenAcquireResultKindCancelled: + var d GitHubTokenAcquireResultCancelled + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case GitHubTokenAcquireResultKindToken: + var d GitHubTokenAcquireResultToken + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawGitHubTokenAcquireResultData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawGitHubTokenAcquireResultData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind GitHubTokenAcquireResultKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r GitHubTokenAcquireResultCancelled) MarshalJSON() ([]byte, error) { + type alias GitHubTokenAcquireResultCancelled + return json.Marshal(struct { + Kind GitHubTokenAcquireResultKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r GitHubTokenAcquireResultToken) MarshalJSON() ([]byte, error) { + type alias GitHubTokenAcquireResultToken + return json.Marshal(struct { + Kind GitHubTokenAcquireResultKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + func (r *HandlePendingToolCallRequest) UnmarshalJSON(data []byte) error { type rawHandlePendingToolCallRequest struct { Error *string `json:"error,omitempty"` @@ -5311,6 +5391,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { ExpAssignments any `json:"expAssignments,omitempty"` FeatureFlags map[string]bool `json:"featureFlags,omitzero"` IncludedBuiltinAgents []string `json:"includedBuiltinAgents,omitzero"` + IncludedBuiltinSkills []string `json:"includedBuiltinSkills,omitzero"` InstalledPlugins []InstalledPlugin `json:"installedPlugins,omitzero"` IntegrationID *string `json:"integrationId,omitempty"` IsExperimentalMode *bool `json:"isExperimentalMode,omitempty"` @@ -5332,6 +5413,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { RemoteSteerable *bool `json:"remoteSteerable,omitempty"` RunningInInteractiveMode *bool `json:"runningInInteractiveMode,omitempty"` SandboxConfig *SandboxConfig `json:"sandboxConfig,omitempty"` + SandboxConfigSource *SandboxConfigSource `json:"sandboxConfigSource,omitempty"` SessionCapabilities []SessionCapability `json:"sessionCapabilities,omitzero"` SessionID *string `json:"sessionId,omitempty"` SessionLimits *SessionLimitsConfig `json:"sessionLimits,omitempty"` @@ -5389,6 +5471,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.ExpAssignments = raw.ExpAssignments r.FeatureFlags = raw.FeatureFlags r.IncludedBuiltinAgents = raw.IncludedBuiltinAgents + r.IncludedBuiltinSkills = raw.IncludedBuiltinSkills r.InstalledPlugins = raw.InstalledPlugins r.IntegrationID = raw.IntegrationID r.IsExperimentalMode = raw.IsExperimentalMode @@ -5410,6 +5493,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.RemoteSteerable = raw.RemoteSteerable r.RunningInInteractiveMode = raw.RunningInInteractiveMode r.SandboxConfig = raw.SandboxConfig + r.SandboxConfigSource = raw.SandboxConfigSource r.SessionCapabilities = raw.SessionCapabilities r.SessionID = raw.SessionID r.SessionLimits = raw.SessionLimits @@ -5573,6 +5657,88 @@ func (r SessionsOpenResumeLast) MarshalJSON() ([]byte, error) { }) } +func unmarshalSettableAuthInfo(data []byte) (SettableAuthInfo, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Type SettableAuthInfoType `json:"type"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Type { + case SettableAuthInfoTypeAPIKey: + var d APIKeyAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SettableAuthInfoTypeCopilotAPIToken: + var d CopilotAPITokenAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SettableAuthInfoTypeEnv: + var d EnvAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SettableAuthInfoTypeGhCLI: + var d GhCLIAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SettableAuthInfoTypeHMAC: + var d HMACAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SettableAuthInfoTypeToken: + var d SettableTokenAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SettableAuthInfoTypeUser: + var d UserAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawSettableAuthInfoData{Discriminator: raw.Type, Raw: data}, nil + } +} + +func (r RawSettableAuthInfoData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Type SettableAuthInfoType `json:"type"` + }{ + Type: r.Discriminator, + }) +} + +func (r SettableTokenAuthInfo) MarshalJSON() ([]byte, error) { + type alias SettableTokenAuthInfo + return json.Marshal(struct { + Type SettableAuthInfoType `json:"type"` + alias + }{ + Type: r.settableAuthInfoType(), + alias: alias(r), + }) +} + func (r *SessionSetCredentialsParams) UnmarshalJSON(data []byte) error { type rawSessionSetCredentialsParams struct { Credentials json.RawMessage `json:"credentials,omitempty"` @@ -5582,7 +5748,7 @@ func (r *SessionSetCredentialsParams) UnmarshalJSON(data []byte) error { return err } if raw.Credentials != nil { - value, err := unmarshalAuthInfo(raw.Credentials) + value, err := unmarshalSettableAuthInfo(raw.Credentials) if err != nil { return err } diff --git a/go/rpc/zsession_encoding.go b/go/rpc/zsession_encoding.go index fb7412d396..5c5d3bca27 100644 --- a/go/rpc/zsession_encoding.go +++ b/go/rpc/zsession_encoding.go @@ -299,6 +299,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeModelCallFinished: + var d ModelCallFinishedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeModelCallStart: var d ModelCallStartData if err := json.Unmarshal(raw.Data, &d); err != nil { diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index 82b0470dbf..2abfd75a66 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -103,6 +103,7 @@ const ( SessionEventTypeMCPResourcesListChanged SessionEventType = "mcp.resources.list_changed" SessionEventTypeMCPToolsListChanged SessionEventType = "mcp.tools.list_changed" SessionEventTypeModelCallFailure SessionEventType = "model.call_failure" + SessionEventTypeModelCallFinished SessionEventType = "model.call_finished" SessionEventTypeModelCallStart SessionEventType = "model.call_start" SessionEventTypePendingMessagesModified SessionEventType = "pending_messages.modified" SessionEventTypePermissionCompleted SessionEventType = "permission.completed" @@ -339,6 +340,8 @@ type AssistantMessageData struct { ParentToolCallID *string `json:"parentToolCallId,omitempty"` // Generation phase for phased-output models (e.g., thinking vs. response phases) Phase *string `json:"phase,omitempty"` + // 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. + ReasoningBlocks *AssistantMessageReasoningBlocks `json:"reasoningBlocks,omitempty"` // Opaque/encrypted extended thinking data from Anthropic models. Session-bound and stripped on resume. ReasoningOpaque *string `json:"reasoningOpaque,omitempty"` // Readable reasoning text from the model's extended thinking @@ -753,6 +756,8 @@ type SessionManagedSettingsResolvedData struct { ManagedKeys []string `json:"managedKeys"` // Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. PermissionsAllowIntersected *bool `json:"permissionsAllowIntersected,omitempty"` + // Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. + SandboxEnabledByUndeterminedPolicy *bool `json:"sandboxEnabledByUndeterminedPolicy,omitempty"` // Whether the server (account/org) managed-settings layer was present ServerManaged bool `json:"serverManaged"` // The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. @@ -942,6 +947,25 @@ type ModelCallFailureData struct { func (*ModelCallFailureData) sessionEventData() {} func (*ModelCallFailureData) Type() SessionEventType { return SessionEventTypeModelCallFailure } +// 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. +type ModelCallFinishedData struct { + // 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. + ContainsBuiltInFileEditRequest *bool `json:"containsBuiltInFileEditRequest,omitempty"` + // 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 + DispatchDurationMs float64 `json:"dispatchDurationMs"` + // Version of the built-in file-edit semantic classifier used for this event + EditClassifierVersion int64 `json:"editClassifierVersion"` + // Identifier of the user interaction that owns the model dispatch, matching assistant.turn_start.interactionId when available + InteractionID *string `json:"interactionId,omitempty"` + // Final outcome after post-response acceptance processing + Outcome ModelCallFinishedOutcome `json:"outcome"` + // Agent-loop iteration within the interaction that initiated the model dispatch + TurnID string `json:"turnId"` +} + +func (*ModelCallFinishedData) sessionEventData() {} +func (*ModelCallFinishedData) Type() SessionEventType { return SessionEventTypeModelCallFinished } + // Hook invocation completion details including output, success status, and error information type HookEndData struct { // Error details when the hook failed @@ -1047,6 +1071,8 @@ type AssistantUsageData struct { NumToolCalls *int64 `json:"numToolCalls,omitempty"` // Number of output tokens produced OutputTokens *int64 `json:"outputTokens,omitempty"` + // Time to first observable model output in milliseconds. Includes text, reasoning, and tool-call output; only available for streaming requests that produce observable output. + OutputTtftMs *float64 `json:"outputTtftMs,omitempty"` // Parent tool call ID when this usage originates from a sub-agent // Deprecated: ParentToolCallID is deprecated. ParentToolCallID *string `json:"parentToolCallId,omitempty"` @@ -1350,6 +1376,8 @@ func (*MCPToolsListChangedData) Type() SessionEventType { return SessionEventTyp type SessionIdleData struct { // True when the preceding agentic loop was cancelled via abort signal Aborted *bool `json:"aborted,omitempty"` + // 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. + Mode *SessionMode `json:"mode,omitempty"` } func (*SessionIdleData) sessionEventData() {} @@ -2105,8 +2133,18 @@ type SubagentCompletedData struct { AgentName string `json:"agentName"` // Whether the sub-agent was torn down by cancellation - its own abort, or an ancestor being killed - instead of finishing its work. Cancellation is not a failure, so the run still reports completion; this distinguishes a torn-down sub-agent from one that ran to the end. Cancelled *bool `json:"cancelled,omitempty"` + // Whether the first model actually dispatched matched the user's configured preference + ConfiguredModelMatchesActual *bool `json:"configuredModelMatchesActual,omitempty"` + // Concrete model the user configured for this sub-agent via `/subagents`, when present + ConfiguredModelPreference *string `json:"configuredModelPreference,omitempty"` // Wall-clock duration of the sub-agent execution in milliseconds DurationMs *int64 `json:"durationMs,omitempty"` + // Whether the explicit task-call model matched the user's configured preference + ExplicitModelMatchesPreference *bool `json:"explicitModelMatchesPreference,omitempty"` + // Explicit model supplied by the parent agent on the task call, when present + ExplicitModelOverride *string `json:"explicitModelOverride,omitempty"` + // First model for which the sub-agent started an inference request, when one was dispatched + FirstDispatchedModel *string `json:"firstDispatchedModel,omitempty"` // Model used by the sub-agent Model *string `json:"model,omitempty"` // Tool call ID of the parent tool invocation that spawned this sub-agent @@ -2126,10 +2164,20 @@ type SubagentFailedData struct { AgentDisplayName string `json:"agentDisplayName"` // Internal name of the sub-agent AgentName string `json:"agentName"` + // Whether the first model actually dispatched matched the user's configured preference + ConfiguredModelMatchesActual *bool `json:"configuredModelMatchesActual,omitempty"` + // Concrete model the user configured for this sub-agent via `/subagents`, when present + ConfiguredModelPreference *string `json:"configuredModelPreference,omitempty"` // Wall-clock duration of the sub-agent execution in milliseconds DurationMs *int64 `json:"durationMs,omitempty"` // Error message describing why the sub-agent failed Error string `json:"error"` + // Whether the explicit task-call model matched the user's configured preference + ExplicitModelMatchesPreference *bool `json:"explicitModelMatchesPreference,omitempty"` + // Explicit model supplied by the parent agent on the task call, when present + ExplicitModelOverride *string `json:"explicitModelOverride,omitempty"` + // First model for which the sub-agent started an inference request, when one was dispatched + FirstDispatchedModel *string `json:"firstDispatchedModel,omitempty"` // Model selected for the sub-agent, when known Model *string `json:"model,omitempty"` // Tool call ID of the parent tool invocation that spawned this sub-agent @@ -2426,6 +2474,15 @@ func (*SessionWorkspaceFileChangedData) Type() SessionEventType { return SessionEventTypeSessionWorkspaceFileChanged } +// Neutral provider-tagged reasoning content blocks preserved verbatim for round-tripping +// Experimental: AssistantMessageReasoningBlocks is part of an experimental API and may change or be removed. +type AssistantMessageReasoningBlocks struct { + // Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest. + Blocks []any `json:"blocks,omitzero"` + // Model provider that produced these reasoning blocks. + Provider string `json:"provider"` +} + // Neutral provider-tagged server-side tool-use payload (tool search, advisor) for verbatim round-tripping // Experimental: AssistantMessageServerTools is part of an experimental API and may change or be removed. type AssistantMessageServerTools struct { @@ -3108,6 +3165,8 @@ type PermissionPromptRequestMCP struct { // Assisted-approval judge information for this request; present only in assisted mode. // Experimental: AssistedApproval is part of an experimental API and may change or be removed. AssistedApproval *PermissionAssistedApproval `json:"assistedApproval,omitempty"` + // 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. + CanOfferServerWideApproval *bool `json:"canOfferServerWideApproval,omitempty"` // Advisory runtime permission recommendation. The host remains responsible for deciding the request and may reject it. // Experimental: PermissionRecommendation is part of an experimental API and may change or be removed. PermissionRecommendation *PermissionRecommendation `json:"permissionRecommendation,omitempty"` @@ -4710,6 +4769,8 @@ const ( ManagedSettingsEnforcedEscalationApproveAll ManagedSettingsEnforcedEscalation = "approve_all" // Assisted mode — keeps normal prompt paths and adds an LLM recommendation, distinct from allow-all. ManagedSettingsEnforcedEscalationAssistedApproval ManagedSettingsEnforcedEscalation = "assisted_approval" + // A server-wide MCP "Always Allow" (or `--allow-tool `) blanket that would auto-approve every tool from an MCP server. Capped to per-tool approval; each tool still prompts. + ManagedSettingsEnforcedEscalationServerWideMCPApproval ManagedSettingsEnforcedEscalation = "server_wide_mcp_approval" // Unrestricted filesystem access outside the session's allowed directories. ManagedSettingsEnforcedEscalationUnrestrictedPaths ManagedSettingsEnforcedEscalation = "unrestricted_paths" // Unrestricted URL fetch access. @@ -4843,6 +4904,20 @@ const ( ModelCallFailureTransportWebsocket ModelCallFailureTransport = "websocket" ) +// Final outcome of one logical model dispatch after response acceptance processing +type ModelCallFinishedOutcome string + +const ( + // The dispatch was cancelled before an accepted response was produced. + ModelCallFinishedOutcomeCancelled ModelCallFinishedOutcome = "cancelled" + // The dispatch ended with a provider or transport error. + ModelCallFinishedOutcomeError ModelCallFinishedOutcome = "error" + // The provider response was rejected during post-response acceptance processing. + ModelCallFinishedOutcomeRejected ModelCallFinishedOutcome = "rejected" + // The provider response was accepted for continued agent processing. + ModelCallFinishedOutcomeSuccess ModelCallFinishedOutcome = "success" +) + // Binary result type discriminator. Use "image" for images and "resource" for other binary data. type OmittedBinaryType string diff --git a/go/session.go b/go/session.go index 600a4bbebc..5d45d19ef2 100644 --- a/go/session.go +++ b/go/session.go @@ -56,42 +56,45 @@ type sessionHandler struct { // }) type Session struct { // SessionID is the unique identifier for this session. - SessionID string - workspacePath string - client *jsonrpc2.Client - clientSessionAPIs *rpc.ClientSessionAPIHandlers - handlers []sessionHandler - nextHandlerID uint64 - handlerMutex sync.RWMutex - toolHandlers map[string]ToolHandler - toolHandlersM sync.RWMutex - permissionHandler PermissionHandlerFunc - permissionMux sync.RWMutex - managedSettings bool - mcpAuthHandler MCPAuthHandler - mcpAuthMu sync.RWMutex - userInputHandler UserInputHandler - userInputMux sync.RWMutex - exitPlanModeHandler ExitPlanModeRequestHandler - exitPlanModeMu sync.RWMutex - autoModeSwitchHandler AutoModeSwitchRequestHandler - autoModeSwitchMu sync.RWMutex - hooks *SessionHooks - hooksMux sync.RWMutex - transformCallbacks map[string]SectionTransformFn - transformMu sync.Mutex - commandHandlers map[string]CommandHandler - commandHandlersMu sync.RWMutex - elicitationHandler ElicitationHandler - elicitationMu sync.RWMutex - canvasHandler CanvasHandler - canvasMu sync.RWMutex - bearerTokenProviders map[string]BearerTokenProvider - bearerTokenMu sync.RWMutex - openCanvases []rpc.OpenCanvasInstance - openCanvasesMu sync.RWMutex - capabilities SessionCapabilities - capabilitiesMu sync.RWMutex + SessionID string + workspacePath string + client *jsonrpc2.Client + clientSessionAPIs *rpc.ClientSessionAPIHandlers + handlers []sessionHandler + nextHandlerID uint64 + handlerMutex sync.RWMutex + toolHandlers map[string]ToolHandler + toolHandlersM sync.RWMutex + permissionHandler PermissionHandlerFunc + permissionMux sync.RWMutex + managedSettings bool + mcpAuthHandler MCPAuthHandler + mcpAuthMu sync.RWMutex + userInputHandler UserInputHandler + userInputMux sync.RWMutex + exitPlanModeHandler ExitPlanModeRequestHandler + exitPlanModeMu sync.RWMutex + autoModeSwitchHandler AutoModeSwitchRequestHandler + autoModeSwitchMu sync.RWMutex + hooks *SessionHooks + hooksMux sync.RWMutex + transformCallbacks map[string]SectionTransformFn + transformMu sync.Mutex + commandHandlers map[string]CommandHandler + commandHandlersMu sync.RWMutex + elicitationHandler ElicitationHandler + elicitationMu sync.RWMutex + canvasHandler CanvasHandler + canvasMu sync.RWMutex + bearerTokenProviders map[string]BearerTokenProvider + bearerTokenMu sync.RWMutex + releaseGitHubTokenProvider func() + gitHubTokenProviderMu sync.Mutex + gitHubTokenProviderReleased bool + openCanvases []rpc.OpenCanvasInstance + openCanvasesMu sync.RWMutex + capabilities SessionCapabilities + capabilitiesMu sync.RWMutex // eventCh serializes user event handler dispatch. dispatchEvent enqueues; // a single goroutine (processEvents) dequeues and invokes handlers in FIFO order. @@ -495,6 +498,9 @@ func (s *Session) SendAndWait(ctx context.Context, options MessageOptions) (*Ses lastAssistantMessage = &eventCopy mu.Unlock() case *SessionIdleData: + if d.Mode != nil && *d.Mode == SessionModeAutopilot { + break + } select { case idleCh <- struct{}{}: default: @@ -1722,11 +1728,9 @@ func (s *Session) GetEvents(ctx context.Context) ([]SessionEvent, error) { // } func (s *Session) Disconnect() error { _, err := s.client.Request(context.Background(), "session.destroy", sessionDestroyRequest{SessionID: s.SessionID}) - if err != nil { - return fmt.Errorf("failed to disconnect session: %w", err) - } s.closeOnce.Do(func() { close(s.eventCh) }) + s.releaseGitHubTokenProviderRegistration() // Clear handlers s.handlerMutex.Lock() @@ -1749,9 +1753,38 @@ func (s *Session) Disconnect() error { s.elicitationHandler = nil s.elicitationMu.Unlock() + if err != nil { + return fmt.Errorf("failed to disconnect session: %w", err) + } return nil } +func (s *Session) releaseGitHubTokenProviderRegistration() { + s.gitHubTokenProviderMu.Lock() + if s.gitHubTokenProviderReleased { + s.gitHubTokenProviderMu.Unlock() + return + } + s.gitHubTokenProviderReleased = true + release := s.releaseGitHubTokenProvider + s.releaseGitHubTokenProvider = nil + s.gitHubTokenProviderMu.Unlock() + if release != nil { + release() + } +} + +func (s *Session) setGitHubTokenProviderRegistrationRelease(release func()) { + s.gitHubTokenProviderMu.Lock() + if !s.gitHubTokenProviderReleased { + s.releaseGitHubTokenProvider = release + s.gitHubTokenProviderMu.Unlock() + return + } + s.gitHubTokenProviderMu.Unlock() + release() +} + // Abort aborts the currently processing message in this session. // // Use this to cancel a long-running request. The session remains valid diff --git a/go/session_test.go b/go/session_test.go index 9c5f4df8c9..74e212c418 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -366,6 +366,138 @@ func readTestJSONRPCFrame(r io.Reader) ([]byte, error) { return data, err } +func TestSession_SendAndWaitSkipsAutopilotContinuationIdle(t *testing.T) { + stdinR, stdinW := io.Pipe() + stdoutR, stdoutW := io.Pipe() + defer stdinR.Close() + defer stdinW.Close() + defer stdoutR.Close() + defer stdoutW.Close() + + client := jsonrpc2.NewClient(stdinW, stdoutR) + client.Start() + defer client.Stop() + + requestReceived := make(chan struct{}) + errCh := make(chan error, 1) + go func() { + frame, err := readTestJSONRPCFrame(stdinR) + if err != nil { + errCh <- err + return + } + + var request struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + } + if err := json.Unmarshal(frame, &request); err != nil { + errCh <- err + return + } + if request.Method != "session.send" { + errCh <- fmt.Errorf("expected session.send, got %s", request.Method) + return + } + + response := map[string]any{ + "jsonrpc": "2.0", + "id": json.RawMessage(request.ID), + "result": map[string]any{"messageId": "message-1"}, + } + data, err := json.Marshal(response) + if err != nil { + errCh <- err + return + } + if _, err := fmt.Fprintf(stdoutW, "Content-Length: %d\r\n\r\n%s", len(data), data); err != nil { + errCh <- err + return + } + close(requestReceived) + }() + + session := &Session{ + SessionID: "session-1", + client: client, + RPC: rpc.NewSessionRPC(client, "session-1"), + handlers: make([]sessionHandler, 0), + eventCh: make(chan SessionEvent, 8), + } + go session.processEvents() + defer close(session.eventCh) + + resultCh := make(chan *SessionEvent, 1) + go func() { + result, err := session.SendAndWait(t.Context(), MessageOptions{Prompt: "keep going"}) + if err != nil { + errCh <- err + return + } + resultCh <- result + }() + + select { + case <-requestReceived: + case err := <-errCh: + t.Fatal(err) + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for session.send request") + } + + continuationIdleProcessed := make(chan struct{}) + unsubscribe := session.On(func(event SessionEvent) { + if idle, ok := event.Data.(*SessionIdleData); ok && + idle.Mode != nil && *idle.Mode == SessionModeAutopilot { + close(continuationIdleProcessed) + } + }) + defer unsubscribe() + + autopilot := SessionModeAutopilot + session.dispatchEvent(SessionEvent{Data: &AssistantMessageData{ + Content: "intermediate", + MessageID: "assistant-1", + }}) + session.dispatchEvent(SessionEvent{Data: &SessionIdleData{Mode: &autopilot}}) + + select { + case <-continuationIdleProcessed: + case err := <-errCh: + t.Fatal(err) + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for autopilot continuation idle") + } + + select { + case <-resultCh: + t.Fatal("SendAndWait returned at an autopilot continuation idle") + default: + } + + interactive := SessionModeInteractive + session.dispatchEvent(SessionEvent{Data: &AssistantMessageData{ + Content: "final", + MessageID: "assistant-2", + }}) + session.dispatchEvent(SessionEvent{Data: &SessionIdleData{Mode: &interactive}}) + + select { + case result := <-resultCh: + message, ok := result.Data.(*AssistantMessageData) + if !ok { + t.Fatalf("expected assistant message, got %T", result.Data) + } + if message.Content != "final" { + t.Fatalf("expected final assistant message, got %q", message.Content) + } + case err := <-errCh: + t.Fatal(err) + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for terminal idle") + } +} + func TestSession_On(t *testing.T) { t.Run("multiple handlers all receive events", func(t *testing.T) { session, cleanup := newTestSession() diff --git a/go/types.go b/go/types.go index 60781d1da8..1d98e06158 100644 --- a/go/types.go +++ b/go/types.go @@ -393,6 +393,16 @@ type PermissionInvocation struct { // may change or be removed. type PermissionDecisionContext = rpc.PermissionDecisionContext +// PermissionResponseCapability describes whether the responding client could +// ask a user for a permission decision. +type PermissionResponseCapability = rpc.PermissionResponseCapability + +const ( + PermissionResponseCapabilityHeadless = rpc.PermissionResponseCapabilityHeadless + PermissionResponseCapabilityInteractive = rpc.PermissionResponseCapabilityInteractive + PermissionResponseCapabilityNone = rpc.PermissionResponseCapabilityNone +) + // PermissionDecisionOutcome describes the disposition of a permission request // as observed by the responding client. type PermissionDecisionOutcome = rpc.PermissionDecisionOutcome @@ -419,6 +429,7 @@ const ( type PermissionDecisionSurface = rpc.PermissionDecisionSurface const ( + PermissionDecisionSurfaceAcp = rpc.PermissionDecisionSurfaceAcp PermissionDecisionSurfaceCopilotApp = rpc.PermissionDecisionSurfaceCopilotApp PermissionDecisionSurfacePromptMode = rpc.PermissionDecisionSurfacePromptMode PermissionDecisionSurfaceSDK = rpc.PermissionDecisionSurfaceSDK @@ -1298,6 +1309,10 @@ type SessionConfig struct { // and discovered skill directories). When false, no skills are loaded regardless // of SkillDirectories or EnableConfigDiscovery settings. EnableSkills *bool + // IncludedBuiltinSkills is the allowlist of runtime-bundled skill names. + // In ModeEmpty, nil excludes all built-in skills; a non-nil list opts the + // named built-ins back in. Skills from other sources remain eligible. + IncludedBuiltinSkills []string // Tools exposes caller-implemented tools to the CLI. A Tool with a nil Handler // is declaration-only; the consumer must resolve its calls via pending tool RPCs. Tools []Tool @@ -1322,6 +1337,9 @@ type SessionConfig struct { // When provided, the SDK can satisfy MCP server OAuth requests with host-provided // token data or cancellation. OnMCPAuthRequest MCPAuthHandler + // GitHubTokenProvider acquires session-scoped GitHub tokens on demand. It + // cannot be combined with GitHubToken. + GitHubTokenProvider GitHubTokenProvider // OnUserInputRequest is a handler for user input requests from the agent (enables ask_user tool) OnUserInputRequest UserInputHandler // Hooks configures hook handlers for session lifecycle events @@ -1569,11 +1587,16 @@ type ManagedSettings struct { } // DisableBypassPermissionsMode is the managed bypass-permissions policy. -type DisableBypassPermissionsMode = rpc.DisableBypassPermissionsMode +// +// The runtime may introduce additional fail-closed modes. Values are serialized +// as strings so callers can use newer modes without waiting for an SDK release. +type DisableBypassPermissionsMode string const ( // DisableBypassPermissionsModeDisable turns off bypass-permissions mode. - DisableBypassPermissionsModeDisable = rpc.DisableBypassPermissionsModeDisable + DisableBypassPermissionsModeDisable DisableBypassPermissionsMode = "disable" + // DisableBypassPermissionsModeAllowAutoOnly permits only automatic bypass. + DisableBypassPermissionsModeAllowAutoOnly DisableBypassPermissionsMode = "allow-auto-only" ) // ManagedSettingsPermissions is the permissions-only managed policy injected @@ -1581,9 +1604,9 @@ const ( // accepts for fetched managed policy (e.g. "Read(**)", "Shell(git push *)"); // malformed rules are rejected by the runtime at session creation. type ManagedSettingsPermissions struct { - // DisableBypassPermissionsMode, when set to "disable", turns off - // bypass-permissions ("yolo") mode for the session. Deny-wins: no other - // layer can re-enable it. + // DisableBypassPermissionsMode restricts bypass-permissions mode for the + // session. See the DisableBypassPermissionsMode constants for known values. + // Newer values are forwarded unchanged so runtime policies remain fail-closed. DisableBypassPermissionsMode DisableBypassPermissionsMode `json:"disableBypassPermissionsMode,omitempty"` // Deny lists operations that must always be denied. Unioned across layers. Deny []string `json:"deny,omitzero"` @@ -1793,6 +1816,9 @@ type ResumeSessionConfig struct { // ClientName identifies the application using the SDK. // Included in the User-Agent header for API requests. ClientName string + // GitHubTokenProvider acquires session-scoped GitHub tokens on demand. It + // cannot be combined with GitHubToken. + GitHubTokenProvider GitHubTokenProvider // Model to use for this session. Can change the model when resuming. Model string // Tools exposes caller-implemented tools to the CLI. A Tool with a nil Handler @@ -1811,6 +1837,10 @@ type ResumeSessionConfig struct { // be selected or invoked unless a custom agent with the same name is // configured. ExcludedBuiltInAgents []string + // IncludedBuiltinSkills is the allowlist of runtime-bundled skill names. + // In ModeEmpty, nil excludes all built-in skills; a non-nil list opts the + // named built-ins back in. Skills from other sources remain eligible. + IncludedBuiltinSkills []string // Provider configures a custom model provider Provider *ProviderConfig // Capi configures provider-scoped CAPI (Copilot API) session options. @@ -2517,6 +2547,7 @@ type createSessionRequest struct { RequestMCPApps *bool `json:"requestMcpApps,omitempty"` GitHubMCPToolConfig *GitHubMCPToolConfig `json:"githubMcpToolConfig,omitempty"` GitHubToken string `json:"gitHubToken,omitempty"` + GitHubTokenProviderRegistrationID string `json:"gitHubTokenProviderRegistrationId,omitempty"` RemoteSession rpc.RemoteSessionMode `json:"remoteSession,omitempty"` Cloud *CloudSessionOptions `json:"cloud,omitempty"` Canvases []CanvasDeclaration `json:"canvases,omitempty"` @@ -2615,6 +2646,7 @@ type resumeSessionRequest struct { RequestMCPApps *bool `json:"requestMcpApps,omitempty"` GitHubMCPToolConfig *GitHubMCPToolConfig `json:"githubMcpToolConfig,omitempty"` GitHubToken string `json:"gitHubToken,omitempty"` + GitHubTokenProviderRegistrationID string `json:"gitHubTokenProviderRegistrationId,omitempty"` RemoteSession rpc.RemoteSessionMode `json:"remoteSession,omitempty"` Canvases []CanvasDeclaration `json:"canvases,omitempty"` OpenCanvases []rpc.OpenCanvasInstance `json:"openCanvases,omitempty"` diff --git a/go/zsession_events.go b/go/zsession_events.go index 711943b45d..11a1bd666f 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -16,6 +16,7 @@ type ( AssistantIntentData = rpc.AssistantIntentData AssistantMessageData = rpc.AssistantMessageData AssistantMessageDeltaData = rpc.AssistantMessageDeltaData + AssistantMessageReasoningBlocks = rpc.AssistantMessageReasoningBlocks AssistantMessageServerTools = rpc.AssistantMessageServerTools AssistantMessageStartData = rpc.AssistantMessageStartData AssistantMessageToolRequest = rpc.AssistantMessageToolRequest @@ -155,6 +156,8 @@ type ( ModelCallFailureRequestFingerprint = rpc.ModelCallFailureRequestFingerprint ModelCallFailureSource = rpc.ModelCallFailureSource ModelCallFailureTransport = rpc.ModelCallFailureTransport + ModelCallFinishedData = rpc.ModelCallFinishedData + ModelCallFinishedOutcome = rpc.ModelCallFinishedOutcome ModelCallStartData = rpc.ModelCallStartData ModelChangeSource = rpc.ModelChangeSource OmittedBinaryOmittedReason = rpc.OmittedBinaryOmittedReason @@ -498,6 +501,7 @@ const ( ManagedSettingsEnforcedEscalationAllowAll = rpc.ManagedSettingsEnforcedEscalationAllowAll ManagedSettingsEnforcedEscalationApproveAll = rpc.ManagedSettingsEnforcedEscalationApproveAll ManagedSettingsEnforcedEscalationAssistedApproval = rpc.ManagedSettingsEnforcedEscalationAssistedApproval + ManagedSettingsEnforcedEscalationServerWideMCPApproval = rpc.ManagedSettingsEnforcedEscalationServerWideMCPApproval ManagedSettingsEnforcedEscalationUnrestrictedPaths = rpc.ManagedSettingsEnforcedEscalationUnrestrictedPaths ManagedSettingsEnforcedEscalationUnrestrictedURLs = rpc.ManagedSettingsEnforcedEscalationUnrestrictedURLs ManagedSettingsResolvedSourceClient = rpc.ManagedSettingsResolvedSourceClient @@ -542,6 +546,10 @@ const ( ModelCallFailureSourceTopLevel = rpc.ModelCallFailureSourceTopLevel ModelCallFailureTransportHTTP = rpc.ModelCallFailureTransportHTTP ModelCallFailureTransportWebsocket = rpc.ModelCallFailureTransportWebsocket + ModelCallFinishedOutcomeCancelled = rpc.ModelCallFinishedOutcomeCancelled + ModelCallFinishedOutcomeError = rpc.ModelCallFinishedOutcomeError + ModelCallFinishedOutcomeRejected = rpc.ModelCallFinishedOutcomeRejected + ModelCallFinishedOutcomeSuccess = rpc.ModelCallFinishedOutcomeSuccess ModelChangeSourceAgent = rpc.ModelChangeSourceAgent ModelChangeSourceAutomatic = rpc.ModelChangeSourceAutomatic ModelChangeSourceConfigCommand = rpc.ModelChangeSourceConfigCommand @@ -660,6 +668,7 @@ const ( SessionEventTypeMCPResourcesListChanged = rpc.SessionEventTypeMCPResourcesListChanged SessionEventTypeMCPToolsListChanged = rpc.SessionEventTypeMCPToolsListChanged SessionEventTypeModelCallFailure = rpc.SessionEventTypeModelCallFailure + SessionEventTypeModelCallFinished = rpc.SessionEventTypeModelCallFinished SessionEventTypeModelCallStart = rpc.SessionEventTypeModelCallStart SessionEventTypePendingMessagesModified = rpc.SessionEventTypePendingMessagesModified SessionEventTypePermissionCompleted = rpc.SessionEventTypePermissionCompleted diff --git a/java/.mvn/wrapper/maven-wrapper.properties b/java/.mvn/wrapper/maven-wrapper.properties index 8dea6c227c..216df05897 100644 --- a/java/.mvn/wrapper/maven-wrapper.properties +++ b/java/.mvn/wrapper/maven-wrapper.properties @@ -1,3 +1,3 @@ wrapperVersion=3.3.4 distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip diff --git a/java/README.md b/java/README.md index 2f8ca3dfa3..d7d6bdf269 100644 --- a/java/README.md +++ b/java/README.md @@ -32,14 +32,14 @@ Replace `${copilot.sdk.version}` with the latest release from Maven Central. com.github copilot-sdk-java - 1.0.12-preview.0 + 1.0.13-preview.1 ``` ### Gradle ```groovy -implementation 'com.github:copilot-sdk-java:1.0.12-preview.0' +implementation 'com.github:copilot-sdk-java:1.0.13-preview.1' ``` #### Snapshot Builds @@ -58,7 +58,7 @@ Snapshot builds of the next development version are published to Maven Central S com.github copilot-sdk-java - 1.0.13-preview.0-SNAPSHOT + 1.0.14-preview.1-SNAPSHOT ``` @@ -67,12 +67,12 @@ Snapshot builds of the next development version are published to Maven Central S Replace `${copilot.sdk.version}` with the latest release from Maven Central. ```groovy -implementation 'com.github:copilot-sdk-java:1.0.13-preview.0-SNAPSHOT' +implementation 'com.github:copilot-sdk-java:1.0.14-preview.1-SNAPSHOT' ``` ## In-process mode (experimental) -The SDK supports running the Copilot runtime **in-process** as a native library instead of spawning a separate CLI process. This eliminates process management overhead and simplifies deployment. In-process mode is currently experimental and only supported on **linux-x64**. +The SDK supports running the Copilot runtime **in-process** as a native library instead of spawning a separate CLI process. This eliminates process management overhead and simplifies deployment. In-process mode is currently experimental and supported on **linux-x64** (glibc), **win32-x64**, and **darwin-arm64**. Because in-process mode is experimental, see the [Using experimental APIs](#using-experimental-apis) section for how to opt in. @@ -88,13 +88,14 @@ Add both the SDK and the platform-specific native runtime to your project: copilot-sdk-java ${copilot.version} - + com.github copilot-sdk-java-runtime ${copilot.version} linux-x64 + net.java.dev.jna @@ -175,6 +176,25 @@ directly. `CopilotClientOptions.setCwd(...)` sets the runtime process working directory, which otherwise inherits the current process working directory. `SessionConfig.setWorkingDirectory(...)` sets the session working directory, which otherwise defaults to the runtime process working directory. +For rotating per-session GitHub credentials, use +`SessionConfig.setGitHubTokenProvider(...)` (or the equivalent +`ResumeSessionConfig` setter) instead of `setGitHubToken(...)`: + +```java +var config = new SessionConfig().setGitHubTokenProvider(args -> + acquireForHost(args.host()).thenApply(token -> + GitHubTokenProviderResult.token(token, 8 * 60 * 60))); +``` + +The remaining lifetime is required and must be positive when the callback +completes; production GitHub tokens typically last eight hours. A static token +and a provider are mutually exclusive. + +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. + ## Permission Handling `PermissionHandler.APPROVE_ALL` approves requests when managed settings are disabled. When `enableManagedSettings` is true, it completes exceptionally. Custom handlers can inspect `request.getManagedApprovalRequired()` for human-facing confirmation logic. @@ -487,6 +507,58 @@ mvn verify -Dskip.test.harness=true mvn jacoco:prepare-agent@wire-up-coverage-instrumentation antrun:run@print-test-jdk-banner surefire:test failsafe:integration-test failsafe:verify jacoco:report@build-coverage-report-from-tests -Denforcer.skip=true ``` +#### Development Setup for native embedding + +Run native-runtime Maven commands from the `java` directory. Native packaging requires Node.js and npm in addition to JDK 25 and Maven because `copilot-native/scripts/fetch-native.mjs` retrieves the pinned npm runtime package. + +On a native Linux x64 glibc host, Maven activates the `native-linux-x64` profile when `copilot.native.libc=glibc` is set. On Windows x64, Maven activates `native-win32-x64` automatically. On Apple Silicon macOS, Maven activates `native-darwin-arm64` automatically. The matching profile validates the host, runs the native script tests, fetches the pinned `@github/copilot-` package during `generate-resources`, packages the classifier JAR during `package`, and verifies its native contents. Ensure npm can authenticate to the package registry before running the build. + +Before opting in, validate that Node.js reports glibc for the build host: + +```bash +node copilot-native/scripts/validate-native-host.mjs linux-x64 +mvn -pl copilot-native clean verify -Dcopilot.native.libc=glibc +``` + +The `inprocess` test profile performs the same validation and native packaging automatically, so the full in-process test command remains: + +```bash +mvn -Pinprocess clean verify +``` + +On Windows PowerShell, initialize Java and run the same profile: + +```powershell +mvn -Pinprocess clean verify +``` + +The same command validates in-process mode on Apple Silicon macOS: + +```bash +node copilot-native/scripts/validate-native-host.mjs darwin-arm64 +mvn -Pinprocess clean verify +``` + +On Intel macOS, Linux ARM64, Linux x64 musl, and other unsupported hosts, do not set `copilot.native.libc=glibc`. A normal build produces only the OS-neutral primary, sources, and Javadoc JARs; it does not run native script tests, download or stage native files, or produce a platform classifier JAR. + +To build only the OS-neutral artifacts on any host, or override the glibc opt-in, disable native download and packaging: + +```bash +mvn -pl copilot-native clean package -DskipTests -Dcopilot.native.libc=glibc -Dcopilot.native.skip.download=true +``` + +The verified Linux x64 checks are: + +```bash +node --test copilot-native/scripts/fetch-native.test.mjs copilot-native/scripts/validate-native-host.test.mjs +mvn -pl copilot-native help:active-profiles -Dcopilot.native.libc=glibc -Dcopilot.native.skip.download=false +mvn -pl copilot-native test -Dcopilot.native.libc=glibc +mvn clean verify -Dcopilot.native.libc=glibc +mvn clean package -pl copilot-native -DskipTests -Dcopilot.native.libc=glibc -Dcopilot.native.skip.download=true +``` + +On Linux x64, the classifier JAR contains `native/linux-x64/runtime.node`, `native/linux-x64/platform.properties`, and `native/linux-x64/copilot`. On Windows x64, it contains `native/win32-x64/runtime.node`, `native/win32-x64/platform.properties`, and `native/win32-x64/copilot.exe`. On Apple Silicon macOS, it contains `native/darwin-arm64/runtime.node`, `native/darwin-arm64/platform.properties`, and `native/darwin-arm64/copilot`. The placeholder JAR remains OS-neutral and contains no native binaries. Unsupported hosts retain the placeholder-only behavior. + ## License MIT — see [LICENSE](sdk/LICENSE) for details. diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index 642f9171a4..4ad0e02929 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.0-SNAPSHOT + 1.0.14-preview.1-SNAPSHOT ../pom.xml @@ -17,7 +17,7 @@ jar GitHub Copilot SDK :: Java :: Native Runtime - Native runtime binaries for the GitHub Copilot Java SDK, published as per-platform classifier JARs + Native runtime artifacts for the GitHub Copilot Java SDK, with host-matched platform classifier JARs https://github.com/github/copilot-sdk @@ -35,12 +35,6 @@ its SHA-512 integrity hash. --> ${project.basedir}/../.. - - linux-x64 ${project.build.directory}/native-staging org.codehaus.mojo exec-maven-plugin - fetch-native-linux-x64 - generate-resources + validate-native-host + none + + exec + + + node + + ${project.basedir}/scripts/validate-native-host.mjs + ${copilot.native.classifier} + + + + + fetch-native + none exec @@ -93,15 +102,18 @@ test-fetch-native - test + none exec + ${skipTests} node --test ${project.basedir}/scripts/fetch-native.test.mjs + ${project.basedir}/scripts/validate-native-host.test.mjs + ${project.basedir}/scripts/validate-native-artifact.test.mjs @@ -114,12 +126,12 @@ - jar-linux-x64 - package + jar-native + none jar @@ -173,7 +185,7 @@ verify-native-jars - package + none run @@ -193,10 +205,10 @@ - + - + @@ -221,6 +233,446 @@ + + + inprocess + + linux-x64 + copilot + + + + + org.codehaus.mojo + exec-maven-plugin + + + validate-native-host + validate + + + fetch-native + generate-resources + + + test-fetch-native + test + + + + + org.apache.maven.plugins + maven-jar-plugin + + + jar-native + package + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + verify-native-jars + package + + + + + + + + + native-linux-x64 + + + Linux + amd64 + + + copilot.native.libc + glibc + + + + linux-x64 + copilot + + + + + org.codehaus.mojo + exec-maven-plugin + + + validate-native-host + validate + + + fetch-native + generate-resources + + + test-fetch-native + test + + + + + org.apache.maven.plugins + maven-jar-plugin + + + jar-native + package + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + verify-native-jars + package + + + + + + + + native-win32-x64 + + + Windows + amd64 + + + + win32-x64 + copilot.exe + + + + + org.codehaus.mojo + exec-maven-plugin + + + validate-native-host + validate + + + fetch-native + generate-resources + + + test-fetch-native + test + + + + + org.apache.maven.plugins + maven-jar-plugin + + + jar-native + package + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + verify-native-jars + package + + + + + + + + native-darwin-arm64 + + + mac + aarch64 + + + + darwin-arm64 + copilot + + + + + org.codehaus.mojo + exec-maven-plugin + + + validate-native-host + validate + + + fetch-native + generate-resources + + + test-fetch-native + test + + + + + org.apache.maven.plugins + maven-jar-plugin + + + jar-native + package + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + verify-native-jars + package + + + + + + + + + attach-external-win32-classifier + + + copilot.native.external.win32.classifier.path + + + + + + org.codehaus.mojo + exec-maven-plugin + + + validate-external-win32-classifier + validate + + exec + + + node + + ${project.basedir}/scripts/validate-native-artifact.mjs + classifier + win32-x64 + ${copilot.native.external.win32.classifier.path} + ${project.build.finalName}-win32-x64.jar + ${copilot.sdk.root} + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-external-win32-classifier + package + + attach-artifact + + + + + ${copilot.native.external.win32.classifier.path} + jar + win32-x64 + + + + + + + + + + + + attach-external-darwin-classifier + + + copilot.native.external.darwin.classifier.path + + + + + + org.codehaus.mojo + exec-maven-plugin + + + validate-external-darwin-classifier + validate + + exec + + + node + + ${project.basedir}/scripts/validate-native-artifact.mjs + classifier + darwin-arm64 + ${copilot.native.external.darwin.classifier.path} + ${project.build.finalName}-darwin-arm64.jar + ${copilot.sdk.root} + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-external-darwin-classifier + package + + attach-artifact + + + + + ${copilot.native.external.darwin.classifier.path} + jar + darwin-arm64 + + + + + + + + + + + + attach-test-linux-classifier + + + copilot.native.test.linux.classifier.path + + + + + + org.codehaus.mojo + exec-maven-plugin + + + validate-test-linux-classifier + validate + + exec + + + node + + ${project.basedir}/scripts/validate-native-artifact.mjs + classifier + linux-x64 + ${copilot.native.test.linux.classifier.path} + ${project.build.finalName}-linux-x64.jar + ${copilot.sdk.root} + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-test-linux-classifier + package + + attach-artifact + + + + + ${copilot.native.test.linux.classifier.path} + jar + linux-x64 + + + + + + + + + + + + local-publication-validation + + + copilot.native.test.local.publication + true + + + + + + org.sonatype.central + central-publishing-maven-plugin + + true + + + + + - ^1.0.81-6 + ^1.0.81-11 true @@ -129,7 +129,7 @@ org.apache.maven.plugins maven-release-plugin - 3.1.1 + 3.3.1 org.apache.maven.plugins @@ -169,7 +169,7 @@ org.codehaus.mojo flatten-maven-plugin - 1.7.0 + 1.8.0 ossrh diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json index 0932178ef1..d0b602e846 100644 --- a/java/scripts/codegen/package-lock.json +++ b/java/scripts/codegen/package-lock.json @@ -6,9 +6,9 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.81-6", + "@github/copilot": "^1.0.81-11", "json-schema": "^0.4.0", - "tsx": "^4.23.1" + "tsx": "^4.23.12" } }, "node_modules/@esbuild/aix-ppc64": { @@ -428,9 +428,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.81-6", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.81-6.tgz", - "integrity": "sha512-hT29nRkf0EJE3N6lqeLOPszbdEyALZ+fjYG9zKX5a3L5r+o+m4/KF+8l2gn2yORNqOzwUYNj2vnVzKqeYYNLGg==", + "version": "1.0.81-11", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.81-11.tgz", + "integrity": "sha512-F7hZ6G6fhWH4uq862mbs2JE3nL0KIVBOc94/EFOdEjux3oHUQst9a06gKibEJS6VRULaYTXzatA1EAgNC9dzFA==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -439,20 +439,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.81-6", - "@github/copilot-darwin-x64": "1.0.81-6", - "@github/copilot-linux-arm64": "1.0.81-6", - "@github/copilot-linux-x64": "1.0.81-6", - "@github/copilot-linuxmusl-arm64": "1.0.81-6", - "@github/copilot-linuxmusl-x64": "1.0.81-6", - "@github/copilot-win32-arm64": "1.0.81-6", - "@github/copilot-win32-x64": "1.0.81-6" + "@github/copilot-darwin-arm64": "1.0.81-11", + "@github/copilot-darwin-x64": "1.0.81-11", + "@github/copilot-linux-arm64": "1.0.81-11", + "@github/copilot-linux-x64": "1.0.81-11", + "@github/copilot-linuxmusl-arm64": "1.0.81-11", + "@github/copilot-linuxmusl-x64": "1.0.81-11", + "@github/copilot-win32-arm64": "1.0.81-11", + "@github/copilot-win32-x64": "1.0.81-11" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.81-6", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.81-6.tgz", - "integrity": "sha512-nALa4e8Jc/g5ltIHrpHBHByJ5rlgzoZFylZIrkQY+B9vr3L57d5F6fOiTbf/OF9blFQX7artWRE1K0TmowGNCA==", + "version": "1.0.81-11", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.81-11.tgz", + "integrity": "sha512-3eLs71CLnJH9RNnESnv4esipZPeGXMlBxQeKXwZY+crwcW2RAR8YuovQyfoZ/5by1PLPbYrOjXNfQL6kXSisrA==", "cpu": [ "arm64" ], @@ -466,9 +466,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.81-6", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.81-6.tgz", - "integrity": "sha512-K+bp799DejrsmxMNyaFAmKo4xnLJXBb8hkv9N8OCQukmTSoRpfqhv2oTDfgVFadwllt+py/FIdxKTZQYvPGGGw==", + "version": "1.0.81-11", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.81-11.tgz", + "integrity": "sha512-GdFLiUC8UL9k6K+woG8AyL3zBafd0Br1TIPDV5iiZsyBtTe4g67FjL7DGCYDt6AN9G43jWdK0M0InoNDVq1K1A==", "cpu": [ "x64" ], @@ -482,9 +482,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.81-6", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.81-6.tgz", - "integrity": "sha512-aEpnfTTjOxpesFo9jqk/phZUivOhNHbdBRfBrS2NiCPrQZFBYUC4wRVo/Xo2PMMQ4J07b6fU7JJQPoUUkKy5Wg==", + "version": "1.0.81-11", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.81-11.tgz", + "integrity": "sha512-C4hcAow5CdaVJITbdtGqFgxWpW7TqwyCPNn8OtcbvdWeiKcfOBDrgkvbsezG98/5Ovs3HWxZRT4oZqCEmGF9Ww==", "cpu": [ "arm64" ], @@ -498,9 +498,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.81-6", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.81-6.tgz", - "integrity": "sha512-NFqonFfJCyA7d3bNoYeLWUQ69zelPr9TTnLpAHCi3scFZqbEvMBDFxW2XsKWwfYuuR9XzfU7/tgOUgq3gmL5aA==", + "version": "1.0.81-11", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.81-11.tgz", + "integrity": "sha512-izu0PwWx+wL4zxacO6cd6r0zMMMQ3pTz+2euWcAd7HCJ/CIR6+YYfjU3TI3TPBZ9zDLZGoxHKYYPfJ1ZbSTzEg==", "cpu": [ "x64" ], @@ -514,9 +514,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.81-6", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.81-6.tgz", - "integrity": "sha512-EE99DFTAgTq6eOFDiiv+OUROD2pDQIrzyyJEfUS9K8JanwNc+Py8vTxrJ0yK0slpJ+Fue5uDRno6c9ys8OM59g==", + "version": "1.0.81-11", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.81-11.tgz", + "integrity": "sha512-uWGUjaxOSMu6dKFWcTvXGUUp76vC3OV7nlSupecRkQ7gA3OxdrrB8rXE5/leC606Hk6oe9Wl7wKCH9EwuZ6afg==", "cpu": [ "arm64" ], @@ -530,9 +530,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.81-6", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.81-6.tgz", - "integrity": "sha512-90HRKx25EjhlQNOCCdbiC0Ck0fSKyp8XUxUoCvuNdoigdUP54ZGS07dkyJiJq9KZaKilxZBSpXiNt8t5ETA2Sg==", + "version": "1.0.81-11", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.81-11.tgz", + "integrity": "sha512-BpWKd/iu1tTyuPR2zuPFs1pVOlley4yt/TXkn6/gVwt52VI0rUxEO+n77RVjDcyMxWaUApbad2VNRPIH77guCA==", "cpu": [ "x64" ], @@ -546,9 +546,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.81-6", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.81-6.tgz", - "integrity": "sha512-1dRSHF/7PFzB+AGORg8BJh2n1+N7+sIJDLqozrC9INWFp1t6ercptIXgJTF/V7UZVGQLO4LBBK1HH/QhzUmrfA==", + "version": "1.0.81-11", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.81-11.tgz", + "integrity": "sha512-GOK3cACgD96m065uJxbgcXL7MQ1qm+wq1qtvGBpprY0r9wTYJG/rVCtayjYB6B57rnaBwPll3+PQ2J1qgfO57A==", "cpu": [ "arm64" ], @@ -562,9 +562,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.81-6", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.81-6.tgz", - "integrity": "sha512-lIbN1mk6Rm9bWrWU4/UfrC5OCga7XcBi2LBz5roDnNcuG8mKEevDcNOtbEYz/TaJg+WBMoTbGfXBVd1hGy2DTA==", + "version": "1.0.81-11", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.81-11.tgz", + "integrity": "sha512-u4K6UU2iGQJqQwsdHNilJBwiaC48PfMsWVFnTSlJD5lCYp9zCk1odrvrcmB3ARYinE/IcXLUosiHaM/ChR/pdA==", "cpu": [ "x64" ], @@ -648,9 +648,9 @@ "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/tsx": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", - "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", "license": "MIT", "dependencies": { "esbuild": "~0.28.0" diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json index 8ceacb3e18..d5669c39f0 100644 --- a/java/scripts/codegen/package.json +++ b/java/scripts/codegen/package.json @@ -7,8 +7,8 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.81-6", + "@github/copilot": "^1.0.81-11", "json-schema": "^0.4.0", - "tsx": "^4.23.1" + "tsx": "^4.23.12" } } diff --git a/java/sdk/jbang-example.java b/java/sdk/jbang-example.java index d0e8e0c29c..cf091d3c12 100644 --- a/java/sdk/jbang-example.java +++ b/java/sdk/jbang-example.java @@ -1,5 +1,5 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS com.github:copilot-sdk-java:1.0.12-preview.0 +//DEPS com.github:copilot-sdk-java:1.0.13-preview.1 import com.github.copilot.CopilotClient; import com.github.copilot.generated.AssistantMessageEvent; import com.github.copilot.generated.SessionUsageInfoEvent; diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index 480ba84bd4..905647fbed 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.0-SNAPSHOT + 1.0.14-preview.1-SNAPSHOT ../pom.xml @@ -51,16 +51,6 @@ mvn verify -Dcopilot.cli.path=/some/other/copilot/npm-loader.js --> ${copilot.sdk.root}/nodejs/node_modules/@github/copilot/npm-loader.js - - ${copilot.sdk.root}/nodejs/node_modules/@github/copilot-linux-x64/copilot false +