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..fd07aae155 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,594 @@ 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-linux-arm64-classifier: + name: Build Linux ARM64 native classifier + needs: prepare-release + runs-on: ubuntu-24.04-arm + 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 linux-arm64 classifier + run: | + 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 linux-arm64 + mvn -B -pl copilot-native package -DskipTests -Dcopilot.native.libc=glibc + VERSION="${{ needs.prepare-release.outputs.release_version }}" + JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-linux-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 linux-arm64 "$JAR" "$(basename "$JAR")" .. + node copilot-native/scripts/validate-native-artifact.mjs placeholder "$PRIMARY_JAR" + MANIFEST="copilot-native/target/linux-arm64-$VERSION.sha256" + HASH=$(sha256sum "$JAR" | cut -d ' ' -f 1) + printf '%s %s' "$HASH" "$(basename "$JAR")" > "$MANIFEST" + node copilot-native/scripts/validate-native-artifact.mjs \ + checksum "$JAR" "$MANIFEST" "$(basename "$JAR")" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: java-native-linux-arm64-release-${{ github.run_id }}-${{ github.run_attempt }} + path: | + java/copilot-native/target/copilot-sdk-java-runtime-${{ needs.prepare-release.outputs.release_version }}-linux-arm64.jar + java/copilot-native/target/linux-arm64-${{ needs.prepare-release.outputs.release_version }}.sha256 + if-no-files-found: error + retention-days: 1 + + 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-windows-arm64-classifier: + name: Build Windows ARM64 native classifier + needs: prepare-release + runs-on: windows-11-arm + 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-arm64 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-arm64 + mvn -B -pl copilot-native package -DskipTests + $version = '${{ needs.prepare-release.outputs.release_version }}' + $jar = "copilot-native/target/copilot-sdk-java-runtime-$version-win32-arm64.jar" + $primaryJar = "copilot-native/target/copilot-sdk-java-runtime-$version.jar" + if (-not (Test-Path -LiteralPath $jar -PathType Leaf)) { + throw "Expected Windows ARM64 classifier was not produced: $jar" + } + node copilot-native/scripts/validate-native-artifact.mjs classifier win32-arm64 $jar ([IO.Path]::GetFileName($jar)) .. + node copilot-native/scripts/validate-native-artifact.mjs placeholder $primaryJar + $manifest = "copilot-native/target/win32-arm64-$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-arm64-release-${{ github.run_id }}-${{ github.run_attempt }} + path: | + java/copilot-native/target/copilot-sdk-java-runtime-${{ needs.prepare-release.outputs.release_version }}-win32-arm64.jar + java/copilot-native/target/win32-arm64-${{ 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: | + 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-linux-arm64-classifier, + build-windows-classifier, + build-windows-arm64-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-linux-arm64-release-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/java-native-linux-arm64 + + - 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-win32-arm64-release-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/java-native-win32-arm64 + + - 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 Linux ARM64 classifier + id: linux-arm64-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-linux-arm64" + JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-linux-arm64.jar" + MANIFEST="$ARTIFACT_DIRECTORY/linux-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 linux-arm64 "$JAR" "$(basename "$JAR")" "$GITHUB_WORKSPACE" + echo "linux_arm64_jar=$JAR" >> "$GITHUB_OUTPUT" + echo "linux_arm64_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT" + + - name: Verify immutable source and Windows classifier + id: windows-artifact run: | - mvn -B release:perform \ - -Dgoals="deploy" \ - -Darguments="-DskipTests -Prelease" + 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: Verify immutable source and Windows ARM64 classifier + id: windows-arm64-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-arm64" + JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-win32-arm64.jar" + MANIFEST="$ARTIFACT_DIRECTORY/win32-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 win32-arm64 "$JAR" "$(basename "$JAR")" "$GITHUB_WORKSPACE" + echo "windows_arm64_jar=$JAR" >> "$GITHUB_OUTPUT" + echo "windows_arm64_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.linux.arm64.classifier.path=${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}" \ + "-Dcopilot.native.external.win32.classifier.path=${{ steps.windows-artifact.outputs.windows_jar }}" \ + "-Dcopilot.native.external.win32.arm64.classifier.path=${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}" \ + "-Dcopilot.native.external.darwin.classifier.path=${{ steps.darwin-artifact.outputs.darwin_jar }}" + 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 "| \`linux-arm64\` | \`ubuntu-24.04-arm\` | \`$(basename "${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}")\` | \`${{ steps.linux-arm64-artifact.outputs.linux_arm64_sha }}\` | Published |" + echo "| \`win32-x64\` | \`windows-latest\` | \`$(basename "${{ steps.windows-artifact.outputs.windows_jar }}")\` | \`${{ steps.windows-artifact.outputs.windows_sha }}\` | Published |" + echo "| \`win32-arm64\` | \`windows-11-arm\` | \`$(basename "${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}")\` | \`${{ steps.windows-arm64-artifact.outputs.windows_arm64_sha }}\` | Published |" + echo "| \`darwin-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-linux-arm64-classifier, + build-windows-classifier, + build-windows-arm64-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 - # Also run Maven release:rollback to clean up any partial release state - mvn -B release:rollback || true + 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 + + 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 +773,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..c0a894dca0 100644 --- a/.github/workflows/java-publish-snapshot.yml +++ b/.github/workflows/java-publish-snapshot.yml @@ -16,8 +16,282 @@ 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-linux-arm64-classifier: + name: Build Linux ARM64 snapshot classifier + needs: resolve-source + runs-on: ubuntu-24.04-arm + permissions: + contents: read + outputs: + version: ${{ steps.build.outputs.version }} + defaults: + run: + shell: bash + working-directory: ./java + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.sha }} + fetch-depth: 1 + persist-credentials: false + + - uses: actions/setup-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 linux-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 linux-arm64 + mvn -B -pl copilot-native package -DskipTests -Dcopilot.native.libc=glibc + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-linux-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 linux-arm64 "$JAR" "$(basename "$JAR")" .. + node copilot-native/scripts/validate-native-artifact.mjs placeholder "$PRIMARY_JAR" + MANIFEST="copilot-native/target/linux-arm64-$VERSION.sha256" + HASH=$(sha256sum "$JAR" | cut -d ' ' -f 1) + printf '%s %s' "$HASH" "$(basename "$JAR")" > "$MANIFEST" + node copilot-native/scripts/validate-native-artifact.mjs \ + checksum "$JAR" "$MANIFEST" "$(basename "$JAR")" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: java-native-linux-arm64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} + path: | + java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-linux-arm64.jar + java/copilot-native/target/linux-arm64-${{ steps.build.outputs.version }}.sha256 + if-no-files-found: error + retention-days: 1 + + 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-windows-arm64-classifier: + name: Build Windows ARM64 snapshot classifier + needs: resolve-source + runs-on: windows-11-arm + 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-arm64 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-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-win32-arm64.jar" + $primaryJar = "copilot-native/target/copilot-sdk-java-runtime-$version.jar" + if (-not (Test-Path -LiteralPath $jar -PathType Leaf)) { + throw "Expected Windows ARM64 classifier was not produced: $jar" + } + node copilot-native/scripts/validate-native-artifact.mjs classifier win32-arm64 $jar ([IO.Path]::GetFileName($jar)) .. + node copilot-native/scripts/validate-native-artifact.mjs placeholder $primaryJar + $manifest = "copilot-native/target/win32-arm64-$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-arm64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} + path: | + java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-win32-arm64.jar + java/copilot-native/target/win32-arm64-${{ steps.build.outputs.version }}.sha256 + if-no-files-found: error + 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-linux-arm64-classifier, + build-windows-classifier, + build-windows-arm64-classifier, + build-darwin-classifier, + ] runs-on: ubuntu-latest defaults: run: @@ -26,12 +300,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 +315,182 @@ 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-linux-arm64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/java-native-linux-arm64 + + - 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-win32-arm64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/java-native-win32-arm64 + + - 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 Linux ARM64 classifier + id: linux-arm64-artifact run: | + SOURCE_COMMIT=$(git rev-parse HEAD) + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.source_sha }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved snapshot source." + exit 1 + fi VERSION=$(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 + if [ "$VERSION" != "${{ needs.build-linux-arm64-classifier.outputs.version }}" ]; then + echo "::error::Linux ARM64 classifier version does not match deploy version." exit 1 fi - echo "### Snapshot Publish" >> $GITHUB_STEP_SUMMARY - echo "- **Version:** $VERSION" >> $GITHUB_STEP_SUMMARY - echo "- **Repository:** Maven Central Snapshots" >> $GITHUB_STEP_SUMMARY + ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-linux-arm64" + JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-linux-arm64.jar" + MANIFEST="$ARTIFACT_DIRECTORY/linux-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 linux-arm64 "$JAR" "$(basename "$JAR")" "$GITHUB_WORKSPACE" + echo "linux_arm64_jar=$JAR" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "linux_arm64_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT" - - name: Deploy Snapshot - working-directory: ./java - run: mvn -B deploy -DskipTests + - 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="${{ steps.linux-arm64-artifact.outputs.version }}" + 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: 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: Verify version, source, and Windows ARM64 classifier + id: windows-arm64-artifact + run: | + SOURCE_COMMIT=$(git rev-parse HEAD) + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.source_sha }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved snapshot source." + exit 1 + fi + VERSION="${{ steps.windows-artifact.outputs.version }}" + if [ "$VERSION" != "${{ needs.build-windows-arm64-classifier.outputs.version }}" ]; then + echo "::error::Windows ARM64 classifier version does not match deploy version." + exit 1 + fi + ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-win32-arm64" + JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-win32-arm64.jar" + MANIFEST="$ARTIFACT_DIRECTORY/win32-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 win32-arm64 "$JAR" "$(basename "$JAR")" "$GITHUB_WORKSPACE" + echo "windows_arm64_jar=$JAR" >> "$GITHUB_OUTPUT" + echo "windows_arm64_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.linux.arm64.classifier.path=${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}" \ + "-Dcopilot.native.external.win32.classifier.path=${{ steps.windows-artifact.outputs.windows_jar }}" \ + "-Dcopilot.native.external.win32.arm64.classifier.path=${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}" \ + "-Dcopilot.native.external.darwin.classifier.path=${{ steps.darwin-artifact.outputs.darwin_jar }}" + 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 "| \`linux-arm64\` | \`ubuntu-24.04-arm\` | \`$(basename "${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}")\` | \`${{ steps.linux-arm64-artifact.outputs.linux_arm64_sha }}\` | Published |" + echo "| \`win32-x64\` | \`windows-latest\` | \`$(basename "${{ steps.windows-artifact.outputs.windows_jar }}")\` | \`${{ steps.windows-artifact.outputs.windows_sha }}\` | Published |" + echo "| \`win32-arm64\` | \`windows-11-arm\` | \`$(basename "${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}")\` | \`${{ steps.windows-arm64-artifact.outputs.windows_arm64_sha }}\` | Published |" + echo "| \`darwin-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..36310d26b5 100644 --- a/.github/workflows/java-sdk-tests.yml +++ b/.github/workflows/java-sdk-tests.yml @@ -18,12 +18,26 @@ 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: ubuntu-24.04-arm + classifier: linux-arm64 + maven-args: -Dcopilot.native.libc=glibc + - os: windows-latest + classifier: win32-x64 + - os: windows-11-arm + classifier: win32-arm64 + - 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,10 +54,13 @@ 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" - run: mvn clean verify -Pinprocess + run: mvn clean verify -Pinprocess ${{ matrix.maven-args }} - name: Generate Test Report Summary if: always() @@ -55,13 +72,352 @@ 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-linux-arm64: + name: "Java Native Publication Input (linux-arm64)" + if: github.event.repository.fork == false + runs-on: ubuntu-24.04-arm + 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 linux-arm64 classifier + id: build + run: | + set -euo pipefail + node copilot-native/scripts/validate-native-host.mjs linux-arm64 + mvn -B -pl copilot-native package -DskipTests -Dcopilot.native.libc=glibc + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-linux-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 linux-arm64 "$JAR" "$(basename "$JAR")" .. + node copilot-native/scripts/validate-native-artifact.mjs placeholder "$PRIMARY_JAR" + MANIFEST="copilot-native/target/linux-arm64-$VERSION.sha256" + HASH=$(sha256sum "$JAR" | cut -d ' ' -f 1) + printf '%s %s' "$HASH" "$(basename "$JAR")" > "$MANIFEST" + node copilot-native/scripts/validate-native-artifact.mjs \ + checksum "$JAR" "$MANIFEST" "$(basename "$JAR")" + echo "source_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: java-native-publication-linux-arm64-${{ github.run_id }}-${{ github.run_attempt }} + path: | + java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-linux-arm64.jar + java/copilot-native/target/linux-arm64-${{ steps.build.outputs.version }}.sha256 + if-no-files-found: error + retention-days: 1 + + 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-windows-arm64: + name: "Java Native Publication Input (win32-arm64)" + if: github.event.repository.fork == false + runs-on: windows-11-arm + 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-arm64 classifier + id: build + run: | + node copilot-native/scripts/validate-native-host.mjs win32-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-win32-arm64.jar" + $primaryJar = "copilot-native/target/copilot-sdk-java-runtime-$version.jar" + if (-not (Test-Path -LiteralPath $jar -PathType Leaf)) { + throw "Expected Windows ARM64 classifier was not produced: $jar" + } + node copilot-native/scripts/validate-native-artifact.mjs classifier win32-arm64 $jar ([IO.Path]::GetFileName($jar)) .. + node copilot-native/scripts/validate-native-artifact.mjs placeholder $primaryJar + $manifest = "copilot-native/target/win32-arm64-$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-arm64-${{ github.run_id }}-${{ github.run_attempt }} + path: | + java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-win32-arm64.jar + java/copilot-native/target/win32-arm64-${{ steps.build.outputs.version }}.sha256 + if-no-files-found: error + 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-linux-arm64, + java-native-publication-windows, + java-native-publication-windows-arm64, + 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-linux-arm64-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ github.workspace }}/java/native-publication-input/linux-arm64 + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: java-native-publication-win32-x64-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ github.workspace }}/java/native-publication-input/windows + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: java-native-publication-win32-arm64-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ github.workspace }}/java/native-publication-input/windows-arm64 + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: java-native-publication-darwin-arm64-${{ github.run_id }}-${{ github.run_attempt }} + 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-linux-arm64.outputs.source_sha }}" + test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-windows.outputs.source_sha }}" + test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-windows-arm64.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-linux-arm64.outputs.version }}" + test "$VERSION" = "${{ needs.java-native-publication-windows-arm64.outputs.version }}" + test "$VERSION" = "${{ needs.java-native-publication-darwin.outputs.version }}" + LINUX_ARM64_DIRECTORY="$GITHUB_WORKSPACE/java/native-publication-input/linux-arm64" + WINDOWS_DIRECTORY="$GITHUB_WORKSPACE/java/native-publication-input/windows" + WINDOWS_ARM64_DIRECTORY="$GITHUB_WORKSPACE/java/native-publication-input/windows-arm64" + DARWIN_DIRECTORY="$GITHUB_WORKSPACE/java/native-publication-input/darwin" + LINUX_ARM64_JAR="$LINUX_ARM64_DIRECTORY/copilot-sdk-java-runtime-$VERSION-linux-arm64.jar" + LINUX_ARM64_MANIFEST="$LINUX_ARM64_DIRECTORY/linux-arm64-$VERSION.sha256" + WINDOWS_JAR="$WINDOWS_DIRECTORY/copilot-sdk-java-runtime-$VERSION-win32-x64.jar" + WINDOWS_MANIFEST="$WINDOWS_DIRECTORY/win32-x64-$VERSION.sha256" + WINDOWS_ARM64_JAR="$WINDOWS_ARM64_DIRECTORY/copilot-sdk-java-runtime-$VERSION-win32-arm64.jar" + WINDOWS_ARM64_MANIFEST="$WINDOWS_ARM64_DIRECTORY/win32-arm64-$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 "$LINUX_ARM64_JAR" "$LINUX_ARM64_MANIFEST" "$(basename "$LINUX_ARM64_JAR")" + node copilot-native/scripts/validate-native-artifact.mjs \ + classifier linux-arm64 "$LINUX_ARM64_JAR" "$(basename "$LINUX_ARM64_JAR")" .. + 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 "$WINDOWS_ARM64_JAR" "$WINDOWS_ARM64_MANIFEST" "$(basename "$WINDOWS_ARM64_JAR")" + node copilot-native/scripts/validate-native-artifact.mjs \ + classifier win32-arm64 "$WINDOWS_ARM64_JAR" "$(basename "$WINDOWS_ARM64_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.linux.arm64.classifier.path=$LINUX_ARM64_JAR" \ + "-Dcopilot.native.external.win32.classifier.path=$WINDOWS_JAR" \ + "-Dcopilot.native.external.win32.arm64.classifier.path=$WINDOWS_ARM64_JAR" \ + "-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 +481,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..c97660f369 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; @@ -5297,6 +5393,32 @@ internal sealed class LogRequest public string? Url { get; set; } } +/// Managed sandbox enforcement state for a session. +[Experimental(Diagnostics.Experimental)] +public sealed class SandboxEnforcementStatus +{ + /// Whether an enforcement failure has permanently blocked the session. + [JsonPropertyName("blocked")] + public bool Blocked { get; set; } + + /// The first sandbox enforcement failure that blocked the session. + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + /// Whether the effective managed policy requires an available sandbox backend. + [JsonPropertyName("required")] + public bool Required { get; set; } +} + +/// Identifies the target session. +[Experimental(Diagnostics.Experimental)] +internal sealed class SessionSandboxGetEnforcementStatusRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Authentication status and account metadata for the session. [Experimental(Diagnostics.Experimental)] public sealed class SessionAuthStatus @@ -5350,13 +5472,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 +5696,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; } @@ -6074,6 +6391,14 @@ public sealed class RunOptions [JsonPropertyName("limits")] public FactoryRunLimits? Limits { get; set; } + /// Whether to emit factory phase names to the session transcript. + [JsonPropertyName("logPhaseNames")] + public bool? LogPhaseNames { get; set; } + + /// Whether to notify the originating session when the factory completes. + [JsonPropertyName("notifyOnComplete")] + public bool? NotifyOnComplete { get; set; } + /// Run identifier whose journal and progress should seed this resumed run. [JsonPropertyName("resumeFromRunId")] public string? ResumeFromRunId { get; set; } @@ -6121,6 +6446,14 @@ internal sealed class FactoryResumeRequest [JsonPropertyName("limits")] public FactoryRunLimits? Limits { get; set; } + /// Whether to emit factory phase names to the session transcript. + [JsonPropertyName("logPhaseNames")] + public bool? LogPhaseNames { get; set; } + + /// Whether to notify the originating session when the factory completes. + [JsonPropertyName("notifyOnComplete")] + public bool? NotifyOnComplete { get; set; } + /// Factory run identifier. [JsonPropertyName("runId")] public string RunId { get; set; } = string.Empty; @@ -6130,6 +6463,65 @@ internal sealed class FactoryResumeRequest public string SessionId { get; set; } = string.Empty; } +/// Options for an internal tool-originated factory invocation. +[Experimental(Diagnostics.Experimental)] +internal sealed class FactoryToolRunOptions +{ + /// Per-invocation resource ceiling overrides. + [JsonPropertyName("limits")] + public FactoryRunLimits? Limits { get; set; } + + /// Run identifier whose journal and progress should seed this resumed run. + [JsonPropertyName("resumeFromRunId")] + public string? ResumeFromRunId { get; set; } +} + +/// Internal parameters for invoking a registered factory from a tool. +[Experimental(Diagnostics.Experimental)] +internal sealed class FactoryToolRunRequest +{ + /// Factory input value. + [JsonPropertyName("args")] + public JsonElement Args { get; set; } + + /// Registered factory name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Tool-originated factory invocation options. + [JsonPropertyName("options")] + public FactoryToolRunOptions? Options { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Opaque identifier of the originating tool call. + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } +} + +/// Internal parameters for resuming a factory run from a tool. +[Experimental(Diagnostics.Experimental)] +internal sealed class FactoryToolResumeRequest +{ + /// Optional per-invocation resource ceiling overrides. + [JsonPropertyName("limits")] + public FactoryRunLimits? Limits { get; set; } + + /// Factory run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Opaque identifier of the originating tool call. + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } +} + /// Parameters for retrieving a factory run. [Experimental(Diagnostics.Experimental)] internal sealed class FactoryGetRunRequest @@ -9384,7 +9776,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; } @@ -10567,6 +10959,10 @@ public sealed class OptionsUpdateAdditionalContentExclusionPolicy [Experimental(Diagnostics.Experimental)] public sealed class CapiSessionOptions { + /// Routing preference used when the session model is `auto`. The runtime persists the preference across cold resume. When omitted, the default routing behavior is used. Resuming an already-resident session cannot change its preference. + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + /// Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting this to `false` is equivalent to the `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. [JsonPropertyName("enableWebSocketResponses")] public bool? EnableWebSocketResponses { get; set; } @@ -10588,6 +10984,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 +11202,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 tool directories discovered on PATH and in toolchain environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common developer-tool caches, config, and toolchains. Writable grants cover scratch caches, the Unix GitHub CLI cache, and Cargo's registry, git store, and lock/tracker files. A relocated CARGO_HOME gets the same narrow split: registry and git are read-write; bin is read-only; the home root, config.toml, and credentials.toml stay ungranted. Set to false to disable every grant listed above; user-installed toolchains and caches then need explicit userPolicy.filesystem readonlyPaths and readwritePaths entries. The working directory (see addCurrentWorkingDirectory), temporary storage, session log paths, and system locations follow their own rules and stay granted. Default: true (enabled by default; set to false to opt out). [JsonPropertyName("allowDevToolAccess")] public bool? AllowDevToolAccess { get; set; } @@ -11035,6 +11435,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 +11499,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; } @@ -11918,10 +12327,6 @@ internal sealed class ToolsGetBuiltinDescriptorsRequest [JsonPropertyName("includeAuthor")] public bool? IncludeAuthor { get; set; } - /// Whether line numbers should be omitted from the view tool descriptor. - [JsonPropertyName("noViewLineNumbers")] - public bool? NoViewLineNumbers { get; set; } - /// Whether descriptors should favor fewer user-intervention prompts. [JsonPropertyName("reduceUserIntervention")] public bool? ReduceUserIntervention { get; set; } @@ -11930,10 +12335,6 @@ internal sealed class ToolsGetBuiltinDescriptorsRequest [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - /// Whether shell commands may only run asynchronously. - [JsonPropertyName("shellAsyncOnlyEnabled")] - public bool? ShellAsyncOnlyEnabled { get; set; } - /// Shell-specific names and description lines for shell tools. [JsonPropertyName("shellConfig")] public ToolsShellDescriptorConfig? ShellConfig { get; set; } @@ -12079,6 +12480,11 @@ public partial class ExternalToolTextResultForLlmContentShellExit : ExternalTool [JsonPropertyName("exitCode")] public required long ExitCode { get; set; } + /// Path reported in the shell session's filesystem namespace when shell output exceeded the configured large-output threshold. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("outputFilePath")] + public string? OutputFilePath { get; set; } + /// Output associated with this shell command, if available. May be partial, truncated, or a preview; not guaranteed to be full output. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("outputPreview")] @@ -13019,6 +13425,10 @@ internal sealed class EnqueueCommandParams [JsonPropertyName("command")] public string Command { get; set; } = string.Empty; + /// Optional user-facing text for the queue row. The command string is shown when omitted. + [JsonPropertyName("displayText")] + public string? DisplayText { get; set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -13485,7 +13895,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 +13988,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 +14880,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 +16647,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 +18632,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 +18727,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 +23057,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 +25665,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 +27255,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 +27419,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 +29468,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 +29554,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); } @@ -30267,6 +30975,12 @@ internal SessionRpc(CopilotSession session) internal CopilotSession Session => _session; + /// Sandbox APIs. + public SandboxApi Sandbox => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; + /// GitHubAuth APIs. public GitHubAuthApi GitHubAuth => field ?? @@ -30639,6 +31353,29 @@ public async Task LogAsync(string message, SessionLogLevel? level = n } } +/// Provides session-scoped Sandbox APIs. +[Experimental(Diagnostics.Experimental)] +public sealed class SandboxApi +{ + private readonly CopilotSession _session; + + internal SandboxApi(CopilotSession session) + { + _session = session; + } + + /// Returns whether managed policy requires sandbox enforcement and whether an enforcement failure has permanently blocked the session. + /// The to monitor for cancellation requests. The default is . + /// Managed sandbox enforcement state for a session. + public async Task GetEnforcementStatusAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionSandboxGetEnforcementStatusRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.sandbox.getEnforcementStatus", [request], cancellationToken); + } +} + /// Provides session-scoped GitHubAuth APIs. [Experimental(Diagnostics.Experimental)] public sealed class GitHubAuthApi @@ -30665,7 +31402,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(); @@ -30973,17 +31710,51 @@ public async Task RunAsync(string name, object args, RunOption /// Resumes a factory run using its persisted name, arguments, journal, and accounting. /// Factory run identifier. /// Optional per-invocation resource ceiling overrides. + /// Whether to notify the originating session when the factory completes. + /// Whether to emit factory phase names to the session transcript. /// The to monitor for cancellation requests. The default is . /// Resolved persisted factory identity and resumed run envelope. - public async Task ResumeAsync(string runId, FactoryRunLimits? limits = null, CancellationToken cancellationToken = default) + public async Task ResumeAsync(string runId, FactoryRunLimits? limits = null, bool? notifyOnComplete = null, bool? logPhaseNames = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(runId); _session.ThrowIfDisposed(); - var request = new FactoryResumeRequest { SessionId = _session.SessionId, RunId = runId, Limits = limits }; + var request = new FactoryResumeRequest { SessionId = _session.SessionId, RunId = runId, Limits = limits, NotifyOnComplete = notifyOnComplete, LogPhaseNames = logPhaseNames }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.resume", [request], cancellationToken); } + /// Internal tool-originated factory invocation. + /// Registered factory name. + /// Factory input value. + /// Tool-originated factory invocation options. + /// Opaque identifier of the originating tool call. + /// The to monitor for cancellation requests. The default is . + /// Complete current or terminal factory run envelope. + internal async Task RunFromToolAsync(string name, object args, FactoryToolRunOptions? options = null, string? toolCallId = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(args); + _session.ThrowIfDisposed(); + + var request = new FactoryToolRunRequest { SessionId = _session.SessionId, Name = name, Args = CopilotClient.ToJsonElementForWire(args)!.Value, Options = options, ToolCallId = toolCallId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.runFromTool", [request], cancellationToken); + } + + /// Internal tool-originated factory resume. + /// Factory run identifier. + /// Optional per-invocation resource ceiling overrides. + /// Opaque identifier of the originating tool call. + /// The to monitor for cancellation requests. The default is . + /// Resolved persisted factory identity and resumed run envelope. + internal async Task ResumeFromToolAsync(string runId, FactoryRunLimits? limits = null, string? toolCallId = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(runId); + _session.ThrowIfDisposed(); + + var request = new FactoryToolResumeRequest { SessionId = _session.SessionId, RunId = runId, Limits = limits, ToolCallId = toolCallId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.factory.resumeFromTool", [request], cancellationToken); + } + /// Gets the current or settled envelope for a factory run. /// Factory run identifier. /// The to monitor for cancellation requests. The default is . @@ -32631,10 +33402,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 +33440,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); } } @@ -32799,22 +33572,20 @@ public async Task ExecuteAsync(string name, object arguments, strin } /// Returns the Rust-owned built-in tool descriptors used to construct the session's offered tool set. - /// Whether line numbers should be omitted from the view tool descriptor. /// Whether descriptors should favor fewer user-intervention prompts. /// Whether tool descriptors should include authoring metadata. /// Whether semantic skill lookup is available. /// Shell-specific names and description lines for shell tools. - /// Whether shell commands may only run asynchronously. /// Whether the configured shell supports PowerShell 7 syntax. /// Default shell timeout in milliseconds. /// Whether background task completion notifications are enabled. /// The to monitor for cancellation requests. The default is . /// Rust-owned built-in tool descriptors for the session. - public async Task GetBuiltinDescriptorsAsync(bool? noViewLineNumbers = null, bool? reduceUserIntervention = null, bool? includeAuthor = null, bool? skillEmbeddingEnabled = null, ToolsShellDescriptorConfig? shellConfig = null, bool? shellAsyncOnlyEnabled = null, bool? shellSupportsPowerShell7Syntax = null, double? shellTimeoutMs = null, bool? backgroundTaskNotificationsEnabled = null, CancellationToken cancellationToken = default) + public async Task GetBuiltinDescriptorsAsync(bool? reduceUserIntervention = null, bool? includeAuthor = null, bool? skillEmbeddingEnabled = null, ToolsShellDescriptorConfig? shellConfig = null, bool? shellSupportsPowerShell7Syntax = null, double? shellTimeoutMs = null, bool? backgroundTaskNotificationsEnabled = null, CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); - var request = new ToolsGetBuiltinDescriptorsRequest { SessionId = _session.SessionId, NoViewLineNumbers = noViewLineNumbers, ReduceUserIntervention = reduceUserIntervention, IncludeAuthor = includeAuthor, SkillEmbeddingEnabled = skillEmbeddingEnabled, ShellConfig = shellConfig, ShellAsyncOnlyEnabled = shellAsyncOnlyEnabled, ShellSupportsPowerShell7Syntax = shellSupportsPowerShell7Syntax, ShellTimeoutMs = shellTimeoutMs, BackgroundTaskNotificationsEnabled = backgroundTaskNotificationsEnabled }; + var request = new ToolsGetBuiltinDescriptorsRequest { SessionId = _session.SessionId, ReduceUserIntervention = reduceUserIntervention, IncludeAuthor = includeAuthor, SkillEmbeddingEnabled = skillEmbeddingEnabled, ShellConfig = shellConfig, ShellSupportsPowerShell7Syntax = shellSupportsPowerShell7Syntax, ShellTimeoutMs = shellTimeoutMs, BackgroundTaskNotificationsEnabled = backgroundTaskNotificationsEnabled }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.tools.getBuiltinDescriptors", [request], cancellationToken); } @@ -32979,14 +33750,15 @@ public async Task ExecuteAsync(string commandName, string /// Enqueues a slash command for FIFO processing on the local session. /// Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO with any in-flight items; if the session is idle, processing kicks off immediately. + /// Optional user-facing text for the queue row. The command string is shown when omitted. /// The to monitor for cancellation requests. The default is . /// Indicates whether the command was accepted into the local execution queue. - public async Task EnqueueAsync(string command, CancellationToken cancellationToken = default) + public async Task EnqueueAsync(string command, string? displayText = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(command); _session.ThrowIfDisposed(); - var request = new EnqueueCommandParams { SessionId = _session.SessionId, Command = command }; + var request = new EnqueueCommandParams { SessionId = _session.SessionId, Command = command, DisplayText = displayText }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.commands.enqueue", [request], cancellationToken); } @@ -33388,8 +34160,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 +35558,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 +35580,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 +35616,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); } } @@ -34849,6 +35640,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.AgentInterruptedCancelPhase), TypeInfoPropertyName = "SessionEventsAgentInterruptedCancelPhase")] [JsonSerializable(typeof(GitHub.Copilot.AgentInterruptedData), TypeInfoPropertyName = "SessionEventsAgentInterruptedData")] [JsonSerializable(typeof(GitHub.Copilot.AgentInterruptedEvent), TypeInfoPropertyName = "SessionEventsAgentInterruptedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseCompletedEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseCompletedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseFailedEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseFailedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseStartedEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseStartedEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantIdleData), TypeInfoPropertyName = "SessionEventsAssistantIdleData")] [JsonSerializable(typeof(GitHub.Copilot.AssistantIdleEvent), TypeInfoPropertyName = "SessionEventsAssistantIdleEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantIntentData), TypeInfoPropertyName = "SessionEventsAssistantIntentData")] @@ -34857,10 +35651,13 @@ 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")] [JsonSerializable(typeof(GitHub.Copilot.AssistantMessageToolRequest), TypeInfoPropertyName = "SessionEventsAssistantMessageToolRequest")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantMessageToolRequestCaller), TypeInfoPropertyName = "SessionEventsAssistantMessageToolRequestCaller")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantMessageToolRequestCallerType), TypeInfoPropertyName = "SessionEventsAssistantMessageToolRequestCallerType")] [JsonSerializable(typeof(GitHub.Copilot.AssistantMessageToolRequestType), TypeInfoPropertyName = "SessionEventsAssistantMessageToolRequestType")] [JsonSerializable(typeof(GitHub.Copilot.AssistantReasoningData), TypeInfoPropertyName = "SessionEventsAssistantReasoningData")] [JsonSerializable(typeof(GitHub.Copilot.AssistantReasoningDeltaData), TypeInfoPropertyName = "SessionEventsAssistantReasoningDeltaData")] @@ -34915,6 +35712,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.AutoModeSwitchRequestedData), TypeInfoPropertyName = "SessionEventsAutoModeSwitchRequestedData")] [JsonSerializable(typeof(GitHub.Copilot.AutoModeSwitchRequestedEvent), TypeInfoPropertyName = "SessionEventsAutoModeSwitchRequestedEvent")] [JsonSerializable(typeof(GitHub.Copilot.AutoModeSwitchResponse), TypeInfoPropertyName = "SessionEventsAutoModeSwitchResponse")] +[JsonSerializable(typeof(GitHub.Copilot.AutoTier), TypeInfoPropertyName = "SessionEventsAutoTier")] [JsonSerializable(typeof(GitHub.Copilot.AutopilotObjectiveChangedOperation), TypeInfoPropertyName = "SessionEventsAutopilotObjectiveChangedOperation")] [JsonSerializable(typeof(GitHub.Copilot.AutopilotObjectiveChangedStatus), TypeInfoPropertyName = "SessionEventsAutopilotObjectiveChangedStatus")] [JsonSerializable(typeof(GitHub.Copilot.BinaryAssetReference), TypeInfoPropertyName = "SessionEventsBinaryAssetReference")] @@ -34979,6 +35777,17 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.FactoryRunStartedEvent), TypeInfoPropertyName = "SessionEventsFactoryRunStartedEvent")] [JsonSerializable(typeof(GitHub.Copilot.FactoryRunUpdatedData), TypeInfoPropertyName = "SessionEventsFactoryRunUpdatedData")] [JsonSerializable(typeof(GitHub.Copilot.FactoryRunUpdatedEvent), TypeInfoPropertyName = "SessionEventsFactoryRunUpdatedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.FusionAttribution), TypeInfoPropertyName = "SessionEventsFusionAttribution")] +[JsonSerializable(typeof(GitHub.Copilot.FusionConversationScope), TypeInfoPropertyName = "SessionEventsFusionConversationScope")] +[JsonSerializable(typeof(GitHub.Copilot.FusionFollowUpAction), TypeInfoPropertyName = "SessionEventsFusionFollowUpAction")] +[JsonSerializable(typeof(GitHub.Copilot.FusionFollowUpRecommendation), TypeInfoPropertyName = "SessionEventsFusionFollowUpRecommendation")] +[JsonSerializable(typeof(GitHub.Copilot.FusionPattern), TypeInfoPropertyName = "SessionEventsFusionPattern")] +[JsonSerializable(typeof(GitHub.Copilot.FusionPhaseKind), TypeInfoPropertyName = "SessionEventsFusionPhaseKind")] +[JsonSerializable(typeof(GitHub.Copilot.FusionPhaseStatus), TypeInfoPropertyName = "SessionEventsFusionPhaseStatus")] +[JsonSerializable(typeof(GitHub.Copilot.FusionPhaseUsage), TypeInfoPropertyName = "SessionEventsFusionPhaseUsage")] +[JsonSerializable(typeof(GitHub.Copilot.FusionProjectionMode), TypeInfoPropertyName = "SessionEventsFusionProjectionMode")] +[JsonSerializable(typeof(GitHub.Copilot.FusionScores), TypeInfoPropertyName = "SessionEventsFusionScores")] +[JsonSerializable(typeof(GitHub.Copilot.FusionTurnKind), TypeInfoPropertyName = "SessionEventsFusionTurnKind")] [JsonSerializable(typeof(GitHub.Copilot.GitHubMcpToolConfig), TypeInfoPropertyName = "SessionEventsGitHubMcpToolConfig")] [JsonSerializable(typeof(GitHub.Copilot.GitHubRepoRef), TypeInfoPropertyName = "SessionEventsGitHubRepoRef")] [JsonSerializable(typeof(GitHub.Copilot.HandoffRepository), TypeInfoPropertyName = "SessionEventsHandoffRepository")] @@ -35028,6 +35837,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")] @@ -35117,6 +35929,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.SkillsLoadedSkill), TypeInfoPropertyName = "SessionEventsSkillsLoadedSkill")] [JsonSerializable(typeof(GitHub.Copilot.SubagentCompletedData), TypeInfoPropertyName = "SessionEventsSubagentCompletedData")] [JsonSerializable(typeof(GitHub.Copilot.SubagentCompletedEvent), TypeInfoPropertyName = "SessionEventsSubagentCompletedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.SubagentConfiguredData), TypeInfoPropertyName = "SessionEventsSubagentConfiguredData")] +[JsonSerializable(typeof(GitHub.Copilot.SubagentConfiguredEvent), TypeInfoPropertyName = "SessionEventsSubagentConfiguredEvent")] [JsonSerializable(typeof(GitHub.Copilot.SubagentDeselectedData), TypeInfoPropertyName = "SessionEventsSubagentDeselectedData")] [JsonSerializable(typeof(GitHub.Copilot.SubagentDeselectedEvent), TypeInfoPropertyName = "SessionEventsSubagentDeselectedEvent")] [JsonSerializable(typeof(GitHub.Copilot.SubagentFailedData), TypeInfoPropertyName = "SessionEventsSubagentFailedData")] @@ -35283,6 +36097,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))] @@ -35366,6 +36181,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(FactoryRunResult))] [JsonSerializable(typeof(FactoryRunSummary))] [JsonSerializable(typeof(FactoryRunTerminal))] +[JsonSerializable(typeof(FactoryToolResumeRequest))] +[JsonSerializable(typeof(FactoryToolRunOptions))] +[JsonSerializable(typeof(FactoryToolRunRequest))] [JsonSerializable(typeof(FleetStartRequest))] [JsonSerializable(typeof(FleetStartResult))] [JsonSerializable(typeof(FolderTrustAddParams))] @@ -35374,6 +36192,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 +36380,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 +36390,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))] @@ -35729,6 +36551,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SandboxConfigUserPolicyNetwork))] [JsonSerializable(typeof(SandboxConfigUserPolicyNetworkProxy))] [JsonSerializable(typeof(SandboxConfigUserPolicySeatbelt))] +[JsonSerializable(typeof(SandboxEnforcementStatus))] [JsonSerializable(typeof(ScheduleAddAtRequest))] [JsonSerializable(typeof(ScheduleAddCronRequest))] [JsonSerializable(typeof(ScheduleAddRequest))] @@ -35865,6 +36688,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionQueueRemoveMostRecentRequest))] [JsonSerializable(typeof(SessionQueueSnapshotRequest))] [JsonSerializable(typeof(SessionRemoteDisableRequest))] +[JsonSerializable(typeof(SessionSandboxGetEnforcementStatusRequest))] [JsonSerializable(typeof(SessionScheduleHasSelfPacedRequest))] [JsonSerializable(typeof(SessionScheduleHydrateRequest))] [JsonSerializable(typeof(SessionScheduleListRequest))] @@ -35949,6 +36773,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..4c7ebc6ffa 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -26,6 +26,9 @@ namespace GitHub.Copilot; IgnoreUnrecognizedTypeDiscriminators = true)] [JsonDerivedType(typeof(AbortEvent), "abort")] [JsonDerivedType(typeof(AgentInterruptedEvent), "agent.interrupted")] +[JsonDerivedType(typeof(AssistantFusionPhaseCompletedEvent), "assistant.fusion_phase_completed")] +[JsonDerivedType(typeof(AssistantFusionPhaseFailedEvent), "assistant.fusion_phase_failed")] +[JsonDerivedType(typeof(AssistantFusionPhaseStartedEvent), "assistant.fusion_phase_started")] [JsonDerivedType(typeof(AssistantIdleEvent), "assistant.idle")] [JsonDerivedType(typeof(AssistantIntentEvent), "assistant.intent")] [JsonDerivedType(typeof(AssistantMessageEvent), "assistant.message")] @@ -68,6 +71,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")] @@ -97,6 +101,10 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(SessionErrorEvent), "session.error")] [JsonDerivedType(typeof(SessionExtensionsLoadedEvent), "session.extensions_loaded")] [JsonDerivedType(typeof(SessionExtensionsAttachmentsPushedEvent), "session.extensions.attachments_pushed")] +[JsonDerivedType(typeof(SessionFusionCompletedEvent), "session.fusion_completed")] +[JsonDerivedType(typeof(SessionFusionResolvedEvent), "session.fusion_resolved")] +[JsonDerivedType(typeof(SessionFusionRouteFailedEvent), "session.fusion_route_failed")] +[JsonDerivedType(typeof(SessionFusionRouteStartedEvent), "session.fusion_route_started")] [JsonDerivedType(typeof(SessionHandoffEvent), "session.handoff")] [JsonDerivedType(typeof(SessionIdleEvent), "session.idle")] [JsonDerivedType(typeof(SessionInfoEvent), "session.info")] @@ -129,6 +137,7 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(SessionWorkspaceFileChangedEvent), "session.workspace_file_changed")] [JsonDerivedType(typeof(SkillInvokedEvent), "skill.invoked")] [JsonDerivedType(typeof(SubagentCompletedEvent), "subagent.completed")] +[JsonDerivedType(typeof(SubagentConfiguredEvent), "subagent.configured")] [JsonDerivedType(typeof(SubagentDeselectedEvent), "subagent.deselected")] [JsonDerivedType(typeof(SubagentFailedEvent), "subagent.failed")] [JsonDerivedType(typeof(SubagentSelectedEvent), "subagent.selected")] @@ -578,6 +587,62 @@ public sealed partial class SessionTaskCompleteEvent : SessionEvent public required SessionTaskCompleteData Data { get; set; } } +/// Experimental transient signal that HydraFusion routing has started for an eligible turn. +/// Represents the session.fusion_route_started event. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionFusionRouteStartedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.fusion_route_started"; + + /// The session.fusion_route_started event payload. + [JsonPropertyName("data")] + public required SessionFusionRouteStartedData Data { get; set; } +} + +/// Experimental durable HydraFusion routing failure and the deterministic concrete fallback selected for the turn. +/// Represents the session.fusion_route_failed event. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionFusionRouteFailedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.fusion_route_failed"; + + /// The session.fusion_route_failed event payload. + [JsonPropertyName("data")] + public required SessionFusionRouteFailedData Data { get; set; } +} + +/// Experimental durable validated HydraFusion route and turn policy. +/// Represents the session.fusion_resolved event. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionFusionResolvedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.fusion_resolved"; + + /// The session.fusion_resolved event payload. + [JsonPropertyName("data")] + public required SessionFusionResolvedData Data { get; set; } +} + +/// Experimental durable aggregate outcome of a HydraFusion turn. +/// Represents the session.fusion_completed event. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionFusionCompletedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.fusion_completed"; + + /// The session.fusion_completed event payload. + [JsonPropertyName("data")] + public required SessionFusionCompletedData Data { get; set; } +} + /// Payload of `user.message` with displayed and model-transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. /// Represents the user.message event. public sealed partial class UserMessageEvent : SessionEvent @@ -656,6 +721,48 @@ public sealed partial class AssistantIntentEvent : SessionEvent public required AssistantIntentData Data { get; set; } } +/// Experimental transient HydraFusion phase/model/role signal. +/// Represents the assistant.fusion_phase_started event. +[Experimental(Diagnostics.Experimental)] +public sealed partial class AssistantFusionPhaseStartedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "assistant.fusion_phase_started"; + + /// The assistant.fusion_phase_started event payload. + [JsonPropertyName("data")] + public required AssistantFusionPhaseStartedData Data { get; set; } +} + +/// Experimental durable HydraFusion phase output and lossless replay checkpoint. +/// Represents the assistant.fusion_phase_completed event. +[Experimental(Diagnostics.Experimental)] +public sealed partial class AssistantFusionPhaseCompletedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "assistant.fusion_phase_completed"; + + /// The assistant.fusion_phase_completed event payload. + [JsonPropertyName("data")] + public required AssistantFusionPhaseCompletedData Data { get; set; } +} + +/// Experimental durable typed HydraFusion phase failure and degradation transition. +/// Represents the assistant.fusion_phase_failed event. +[Experimental(Diagnostics.Experimental)] +public sealed partial class AssistantFusionPhaseFailedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "assistant.fusion_phase_failed"; + + /// The assistant.fusion_phase_failed event payload. + [JsonPropertyName("data")] + public required AssistantFusionPhaseFailedData Data { get; set; } +} + /// Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message. /// Represents the assistant.server_tool_progress event. public sealed partial class AssistantServerToolProgressEvent : SessionEvent @@ -825,6 +932,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 @@ -968,6 +1088,19 @@ public sealed partial class SubagentStartedEvent : SessionEvent public required SubagentStartedData Data { get; set; } } +/// Resolved runtime configuration for a configured sub-agent. +/// Represents the subagent.configured event. +public sealed partial class SubagentConfiguredEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "subagent.configured"; + + /// The subagent.configured event payload. + [JsonPropertyName("data")] + public required SubagentConfiguredData Data { get; set; } +} + /// Sub-agent completion details for successful execution. /// Represents the subagent.completed event. public sealed partial class SubagentCompletedEvent : SessionEvent @@ -1783,6 +1916,11 @@ public sealed partial class SessionStartData [JsonPropertyName("alreadyInUse")] public bool? AlreadyInUse { get; set; } + /// Auto routing preference selected at session creation time. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + /// Working directory and git context at session start. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("context")] @@ -1862,6 +2000,11 @@ public sealed partial class SessionResumeData [JsonPropertyName("alreadyInUse")] public bool? AlreadyInUse { get; set; } + /// Auto routing preference active at resume time. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + /// Updated working directory and git context at resume time. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("context")] @@ -1988,6 +2131,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. @@ -2584,6 +2732,11 @@ public sealed partial class SessionCompactionStartData /// Conversation compaction results including success status, metrics, and optional error details. public sealed partial class SessionCompactionCompleteData { + /// Canonical model identifier used for model-specific behavior when replaying compaction. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("behaviorModelId")] + public string? BehaviorModelId { get; set; } + /// Checkpoint snapshot number created for recovery. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("checkpointNumber")] @@ -2713,6 +2866,237 @@ public sealed partial class SessionTaskCompleteData public string? Summary { get; set; } } +/// Experimental transient signal that HydraFusion routing has started for an eligible turn. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionFusionRouteStartedData +{ + /// Identifier for this routing attempt before a durable Fusion turn exists. + [JsonPropertyName("attemptId")] + public required string AttemptId { get; set; } + + /// HydraFusion routing policy requested for the turn. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("policy")] + public string? Policy { get; set; } + + /// Synthetic HydraFusion model selected for the session. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("syntheticModel")] + public string? SyntheticModel { get; set; } + + /// Kind of turn being routed. + [JsonPropertyName("turnKind")] + public required FusionTurnKind TurnKind { get; set; } +} + +/// Experimental durable HydraFusion routing failure and the deterministic concrete fallback selected for the turn. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionFusionRouteFailedData +{ + /// Identifier of the routing attempt that failed. + [JsonPropertyName("attemptId")] + public required string AttemptId { get; set; } + + /// Provider or validation error detail, when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("errorMessage")] + public string? ErrorMessage { get; set; } + + /// Concrete model selected as the deterministic fallback. + [JsonPropertyName("fallbackModel")] + public required string FallbackModel { get; set; } + + /// HydraFusion routing policy requested for the turn. + [JsonPropertyName("policy")] + public required string Policy { get; set; } + + /// Stable machine-readable reason for the routing failure. + [JsonPropertyName("reason")] + public required string Reason { get; set; } + + /// Elapsed routing time in milliseconds before the failure. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("routingLatencyMs")] + public double? RoutingLatencyMs { get; set; } + + /// Synthetic HydraFusion model selected for the session. + [JsonPropertyName("syntheticModel")] + public required string SyntheticModel { get; set; } +} + +/// Experimental durable validated HydraFusion route and turn policy. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionFusionResolvedData +{ + /// Version of the validated HydraFusion event contract. + [JsonPropertyName("contractVersion")] + public required long ContractVersion { get; set; } + + /// Concrete model used when the planned primary model cannot execute. + [JsonPropertyName("fallbackModel")] + public required string FallbackModel { get; set; } + + /// Router recommendation controlling reuse or rerouting on later turns. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("followUp")] + public FusionFollowUpRecommendation? FollowUp { get; set; } + + /// Concrete model recommended for eligible follow-up turns. + [JsonPropertyName("followUpModel")] + public required string FollowUpModel { get; set; } + + /// Stable identifier for the resolved HydraFusion turn. + [JsonPropertyName("fusionId")] + public required string FusionId { get; set; } + + /// Version of the executable model universe used for selection. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("modelUniverseVersion")] + public string? ModelUniverseVersion { get; set; } + + /// Validated orchestration pattern selected for the turn. + [JsonPropertyName("pattern")] + public required FusionPattern Pattern { get; set; } + + /// Version of the validated execution-plan format. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("planVersion")] + public string? PlanVersion { get; set; } + + /// HydraFusion routing policy used to resolve the plan. + [JsonPropertyName("policy")] + public required string Policy { get; set; } + + /// Version of the local routing policy. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("policyVersion")] + public string? PolicyVersion { get; set; } + + /// Concrete model selected for the primary solver phase. + [JsonPropertyName("primaryModel")] + public required string PrimaryModel { get; set; } + + /// Router implementation that supplied the plan. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("routeSource")] + public string? RouteSource { get; set; } + + /// Elapsed time in milliseconds required to resolve and validate the route. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("routingLatencyMs")] + public double? RoutingLatencyMs { get; set; } + + /// Identifier of the local policy rule that matched. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("ruleId")] + public string? RuleId { get; set; } + + /// Zero-based index of the local policy rule that matched. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("ruleIndex")] + public long? RuleIndex { get; set; } + + /// Human-readable name of the local policy rule that matched. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("ruleName")] + public string? RuleName { get; set; } + + /// Validated capability scores used to select the route. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("scores")] + public FusionScores? Scores { get; set; } + + /// Concrete model selected for the review or judge phase, when required. + [JsonPropertyName("secondaryModel")] + public string? SecondaryModel { get; set; } + + /// Synthetic HydraFusion model selected for the session. + [JsonPropertyName("syntheticModel")] + public required string SyntheticModel { get; set; } + + /// Identifier of the session turn associated with the route. + [JsonPropertyName("turnId")] + public required string TurnId { get; set; } +} + +/// Experimental durable aggregate outcome of a HydraFusion turn. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionFusionCompletedData +{ + /// Total cached input tokens reported across all phases. + [JsonPropertyName("cachedTokens")] + public required long CachedTokens { get; set; } + + /// Total tokens written to prompt cache across all phases. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("cacheWriteTokens")] + public long? CacheWriteTokens { get; set; } + + /// Idempotency identifier for the authoritative final commit. + [JsonPropertyName("commitId")] + public required string CommitId { get; set; } + + /// Reason the turn used a degraded route, when applicable. + [JsonPropertyName("degradedReason")] + public string? DegradedReason { get; set; } + + /// Total elapsed execution time for the HydraFusion turn in milliseconds. + [JsonPropertyName("durationMs")] + public required double DurationMs { get; set; } + + /// Concrete model that supplied the authoritative final content. + [JsonPropertyName("finalSourceModel")] + public string? FinalSourceModel { get; set; } + + /// Phase whose output supplied the authoritative final content. + [JsonPropertyName("finalSourcePhaseId")] + public string? FinalSourcePhaseId { get; set; } + + /// Concrete model recommended for eligible follow-up turns. + [JsonPropertyName("followUpModel")] + public required string FollowUpModel { get; set; } + + /// Stable identifier for the completed HydraFusion turn. + [JsonPropertyName("fusionId")] + public required string FusionId { get; set; } + + /// Total input tokens consumed across all phases. + [JsonPropertyName("inputTokens")] + public required long InputTokens { get; set; } + + /// Stable aggregate outcome of the HydraFusion turn. + [JsonPropertyName("outcome")] + public required string Outcome { get; set; } + + /// Total output tokens produced across all phases. + [JsonPropertyName("outputTokens")] + public required long OutputTokens { get; set; } + + /// HydraFusion orchestration pattern executed for the turn. + [JsonPropertyName("pattern")] + public required FusionPattern Pattern { get; set; } + + /// Number of concrete phases attempted by the turn. + [JsonPropertyName("phaseCount")] + public required long PhaseCount { get; set; } + + /// Total concrete model requests made across all phases. + [JsonPropertyName("requestCount")] + public required long RequestCount { get; set; } + + /// Synthetic HydraFusion model selected for the session. + [JsonPropertyName("syntheticModel")] + public required string SyntheticModel { get; set; } + + /// Total normalized AI-unit cost reported across all phases, in nano-AIU. + [JsonPropertyName("totalNanoAiu")] + public required double TotalNanoAiu { get; set; } + + /// Identifier of the session turn associated with the completion. + [JsonPropertyName("turnId")] + public required string TurnId { get; set; } +} + /// Payload of `user.message` with displayed and model-transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. public sealed partial class UserMessageData { @@ -2893,6 +3277,161 @@ public sealed partial class AssistantIntentData public required string Intent { get; set; } } +/// Experimental transient HydraFusion phase/model/role signal. +[Experimental(Diagnostics.Experimental)] +public sealed partial class AssistantFusionPhaseStartedData +{ + /// Conversation scope in which the phase executes. + [JsonPropertyName("conversationScope")] + public required FusionConversationScope ConversationScope { get; set; } + + /// Identifier of the HydraFusion turn containing the phase. + [JsonPropertyName("fusionId")] + public required string FusionId { get; set; } + + /// Concrete model executing the phase. + [JsonPropertyName("model")] + public required string Model { get; set; } + + /// HydraFusion orchestration pattern containing the phase. + [JsonPropertyName("pattern")] + public required FusionPattern Pattern { get; set; } + + /// Stable identifier for the concrete phase. + [JsonPropertyName("phaseId")] + public required string PhaseId { get; set; } + + /// Kind of phase being executed. + [JsonPropertyName("phaseKind")] + public required FusionPhaseKind PhaseKind { get; set; } + + /// Semantic role assigned to the phase. + [JsonPropertyName("role")] + public required string Role { get; set; } +} + +/// Experimental durable HydraFusion phase output and lossless replay checkpoint. +[Experimental(Diagnostics.Experimental)] +public sealed partial class AssistantFusionPhaseCompletedData +{ + /// Provider-normalized textual output produced by the phase. + [JsonPropertyName("content")] + public required string Content { get; set; } + + /// Conversation scope in which the phase executed. + [JsonPropertyName("conversationScope")] + public required FusionConversationScope ConversationScope { get; set; } + + /// Elapsed execution time for the phase in milliseconds. + [JsonPropertyName("durationMs")] + public required double DurationMs { get; set; } + + /// Identifier of the HydraFusion turn containing the phase. + [JsonPropertyName("fusionId")] + public required string FusionId { get; set; } + + /// Concrete model that executed the phase. + [JsonPropertyName("model")] + public required string Model { get; set; } + + /// Stable identifier for the completed phase. + [JsonPropertyName("phaseId")] + public required string PhaseId { get; set; } + + /// Kind of phase that completed. + [JsonPropertyName("phaseKind")] + public required FusionPhaseKind PhaseKind { get; set; } + + /// Exact provider-normalized message used to reconstruct canonical model history. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] + [JsonPropertyName("projectionMessage")] + internal JsonElement? ProjectionMessage { get; set; } + + /// Projection action for the exact internal message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] + [JsonPropertyName("projectionMode")] + internal FusionProjectionMode? ProjectionMode { get; set; } + + /// Semantic role assigned to the completed phase. + [JsonPropertyName("role")] + public required string Role { get; set; } + + /// Terminal request held outside canonical state until selected by the final commit. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] + [JsonPropertyName("stagedTerminal")] + internal FusionStagedTerminal? StagedTerminal { get; set; } + + /// Durable outcome status of the phase. + [JsonPropertyName("status")] + public required FusionPhaseStatus Status { get; set; } + + /// Aggregate concrete-model usage consumed by the phase. + [JsonPropertyName("usage")] + public required FusionPhaseUsage Usage { get; set; } + + /// Structured judge or critic verdict, when the phase produces one. + [JsonPropertyName("verdict")] + public string? Verdict { get; set; } +} + +/// Experimental durable typed HydraFusion phase failure and degradation transition. +[Experimental(Diagnostics.Experimental)] +public sealed partial class AssistantFusionPhaseFailedData +{ + /// Conversation scope in which the phase executed. + [JsonPropertyName("conversationScope")] + public required FusionConversationScope ConversationScope { get; set; } + + /// Identifier of the fallback phase used to continue the turn after degradation. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("degradedToPhaseId")] + public string? DegradedToPhaseId { get; set; } + + /// Elapsed execution time before the phase failed, in milliseconds. + [JsonPropertyName("durationMs")] + public required double DurationMs { get; set; } + + /// Provider or execution error detail, when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("errorMessage")] + public string? ErrorMessage { get; set; } + + /// Identifier of the HydraFusion turn containing the phase. + [JsonPropertyName("fusionId")] + public required string FusionId { get; set; } + + /// Concrete model that attempted the phase. + [JsonPropertyName("model")] + public required string Model { get; set; } + + /// Stable identifier for the failed phase. + [JsonPropertyName("phaseId")] + public required string PhaseId { get; set; } + + /// Kind of phase that failed. + [JsonPropertyName("phaseKind")] + public required FusionPhaseKind PhaseKind { get; set; } + + /// Stable machine-readable reason for the phase failure. + [JsonPropertyName("reason")] + public required string Reason { get; set; } + + /// Semantic role assigned to the failed phase. + [JsonPropertyName("role")] + public required string Role { get; set; } + + /// Durable outcome status of the phase. + [JsonPropertyName("status")] + public required FusionPhaseStatus Status { get; set; } + + /// Aggregate concrete-model usage consumed before the failure. + [JsonPropertyName("usage")] + public required FusionPhaseUsage Usage { get; set; } +} + /// Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message. public sealed partial class AssistantServerToolProgressData { @@ -3006,6 +3545,12 @@ public sealed partial class AssistantMessageData [JsonPropertyName("encryptedContent")] public string? EncryptedContent { get; set; } + /// Experimental HydraFusion source attribution for this ordinary authoritative assistant message. + [Experimental(Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("fusion")] + public FusionAttribution? Fusion { get; set; } + /// CAPI interaction ID for correlating this message with upstream telemetry. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("interactionId")] @@ -3039,6 +3584,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")] @@ -3225,6 +3775,12 @@ public sealed partial class AssistantUsageData [JsonPropertyName("frontierSource")] internal string? FrontierSource { get; set; } + /// Experimental HydraFusion attribution for this concrete model call's usage. + [Experimental(Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("fusion")] + public FusionAttribution? Fusion { get; set; } + /// What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("initiator")] @@ -3281,9 +3837,15 @@ public sealed partial class AssistantUsageData [JsonPropertyName("outputTokens")] public long? OutputTokens { get; set; } - /// Parent tool call ID when this usage originates from a sub-agent. - [EditorBrowsable(EditorBrowsableState.Never)] -#if NET5_0_OR_GREATER + /// 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 [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] #endif [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -3529,6 +4091,12 @@ public sealed partial class ModelCallFailureData [JsonPropertyName("failureKind")] public ModelCallFailureKind? FailureKind { get; set; } + /// Experimental HydraFusion attribution for this failed concrete model call. + [Experimental(Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("fusion")] + public FusionAttribution? Fusion { get; set; } + /// What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("initiator")] @@ -3610,9 +4178,46 @@ 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 { + /// Experimental HydraFusion attribution for this concrete model call. + [Experimental(Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("fusion")] + public FusionAttribution? Fusion { get; set; } + /// Model identifier used for this API call, when known. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("model")] @@ -3667,6 +4272,12 @@ public sealed partial class ToolExecutionStartData [JsonPropertyName("displayVerbatim")] public bool? DisplayVerbatim { get; set; } + /// Experimental HydraFusion attribution for this tool execution. + [Experimental(Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("fusion")] + public FusionAttribution? Fusion { get; set; } + /// Name of the MCP server hosting this tool, when the tool is an MCP tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("mcpServerName")] @@ -3752,6 +4363,12 @@ public sealed partial class ToolExecutionCompleteData [JsonPropertyName("error")] public ToolExecutionCompleteError? Error { get; set; } + /// Experimental HydraFusion attribution for this tool completion. + [Experimental(Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("fusion")] + public FusionAttribution? Fusion { get; set; } + /// CAPI interaction ID for correlating this tool execution with upstream telemetry. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("interactionId")] @@ -3902,6 +4519,16 @@ public sealed partial class SubagentStartedData [JsonPropertyName("agentName")] public required string AgentName { get; set; } + /// Type of the sub-agent selected at spawn time. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("agentType")] + public string? AgentType { get; set; } + + /// Whether the sub-agent runs synchronously or in the background. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("executionMode")] + public string? ExecutionMode { get; set; } + /// Root id of the factory run that spawned this sub-agent, when it was spawned by one. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("factoryRunId")] @@ -3912,11 +4539,43 @@ public sealed partial class SubagentStartedData [JsonPropertyName("model")] public string? Model { get; set; } + /// Task-registry ID of the spawning sub-agent. Absent when the root session spawned this child. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("parentId")] + public string? ParentId { get; set; } + + /// Whether this sub-agent can be resumed. Currently always false. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("resumable")] + public bool? Resumable { get; set; } + /// Tool call ID of the parent tool invocation that spawned this sub-agent. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } } +/// Resolved runtime configuration for a configured sub-agent. +public sealed partial class SubagentConfiguredData +{ + /// Resolved context tier, when configured for the model. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("contextTier")] + public string? ContextTier { get; set; } + + /// Resolved model the sub-agent will run with. + [JsonPropertyName("model")] + public required string Model { get; set; } + + /// Whether the sub-agent accepts follow-up turns. + [JsonPropertyName("multiTurn")] + public required bool MultiTurn { get; set; } + + /// Resolved reasoning effort, when configured for the model. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } +} + /// Sub-agent completion details for successful execution. public sealed partial class SubagentCompletedData { @@ -3933,12 +4592,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 +4654,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 +4674,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")] @@ -4036,6 +4745,11 @@ public sealed partial class HookStartData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("input")] public JsonElement? Input { get; set; } + + /// Tool call ID of the parent tool invocation when this event originates from a sub-agent. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("parentToolCallId")] + public string? ParentToolCallId { get; set; } } /// Hook invocation completion details including output, success status, and error information. @@ -4059,6 +4773,11 @@ public sealed partial class HookEndData [JsonPropertyName("output")] public JsonElement? Output { get; set; } + /// Tool call ID of the parent tool invocation when this event originates from a sub-agent. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("parentToolCallId")] + public string? ParentToolCallId { get; set; } + /// Whether the hook completed successfully. [JsonPropertyName("success")] public required bool Success { get; set; } @@ -4721,6 +5440,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; } @@ -5482,6 +6206,42 @@ public sealed partial class CompactionCompleteCompactionTokensUsed public long? OutputTokens { get; set; } } +/// Durable server recommendation for subsequent HydraFusion turns. +/// Nested data type for FusionFollowUpRecommendation. +[Experimental(Diagnostics.Experimental)] +public sealed partial class FusionFollowUpRecommendation +{ + /// Recommended routing action for the next compaction turn. + [JsonPropertyName("compactionTurn")] + public required FusionFollowUpAction CompactionTurn { get; set; } + + /// Recommended routing action for the next user-message turn. + [JsonPropertyName("userTurn")] + public required FusionFollowUpAction UserTurn { get; set; } +} + +/// Validated HydraFusion routing capability scores. +/// Nested data type for FusionScores. +[Experimental(Diagnostics.Experimental)] +public sealed partial class FusionScores +{ + /// Code-generation capability score returned by the authenticated router. + [JsonPropertyName("codeGen")] + public required double CodeGen { get; set; } + + /// Debugging capability score returned by the authenticated router. + [JsonPropertyName("debugging")] + public required double Debugging { get; set; } + + /// Reasoning capability score returned by the authenticated router. + [JsonPropertyName("reasoning")] + public required double Reasoning { get; set; } + + /// Tool-use capability score returned by the authenticated router. + [JsonPropertyName("toolUse")] + public required double ToolUse { get; set; } +} + /// Optional line range to scope the attachment to a specific section of the file. /// Nested data type for AttachmentFileLineRange. public sealed partial class AttachmentFileLineRange @@ -6032,6 +6792,63 @@ public partial class Attachment } +/// Internal durable terminal request staged by a HydraFusion phase until an idempotent final commit selects it. +/// Nested data type for FusionStagedTerminal. +[Experimental(Diagnostics.Experimental)] +internal sealed partial class FusionStagedTerminal +{ + /// Gets or sets the arguments value. + [JsonPropertyName("arguments")] + public required string Arguments { get; set; } + + /// Gets or sets the assistantMessage value. + [JsonPropertyName("assistantMessage")] + public required JsonElement AssistantMessage { get; set; } + + /// Gets or sets the phaseId value. + [JsonPropertyName("phaseId")] + public required string PhaseId { get; set; } + + /// Gets or sets the toolCallId value. + [JsonPropertyName("toolCallId")] + public required string ToolCallId { get; set; } + + /// Gets or sets the toolName value. + [JsonPropertyName("toolName")] + public required string ToolName { get; set; } +} + +/// Aggregate concrete-model usage for one HydraFusion phase. +/// Nested data type for FusionPhaseUsage. +[Experimental(Diagnostics.Experimental)] +public sealed partial class FusionPhaseUsage +{ + /// Total cached input tokens reported for the phase. + [JsonPropertyName("cachedTokens")] + public required long CachedTokens { get; set; } + + /// Total tokens written to prompt cache during the phase. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("cacheWriteTokens")] + public long? CacheWriteTokens { get; set; } + + /// Total input tokens consumed by the phase. + [JsonPropertyName("inputTokens")] + public required long InputTokens { get; set; } + + /// Total output tokens produced by the phase. + [JsonPropertyName("outputTokens")] + public required long OutputTokens { get; set; } + + /// Number of concrete model requests made by the phase. + [JsonPropertyName("requestCount")] + public required long RequestCount { get; set; } + + /// Total normalized AI-unit cost reported for the phase, in nano-AIU. + [JsonPropertyName("totalNanoAiu")] + public required double TotalNanoAiu { get; set; } +} + /// A source that backs one or more cited spans in the assistant's response. /// Nested data type for CitationSource. [Experimental(Diagnostics.Experimental)] @@ -6189,6 +7006,78 @@ public sealed partial class Citations public required CitationSpan[] Spans { get; set; } } +/// Experimental attribution linking an ordinary event to the HydraFusion turn, phase, and concrete source that produced it. +/// Nested data type for FusionAttribution. +[Experimental(Diagnostics.Experimental)] +public sealed partial class FusionAttribution +{ + /// Idempotency identifier for the authoritative commit, when the event belongs to the selected output. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("commitId")] + public string? CommitId { get; set; } + + /// Conversation scope in which the concrete phase executed. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("conversationScope")] + public string? ConversationScope { get; set; } + + /// Stable identifier for the HydraFusion turn that produced the event. + [JsonPropertyName("fusionId")] + public required string FusionId { get; set; } + + /// HydraFusion orchestration pattern selected for the turn. + [JsonPropertyName("pattern")] + public required string Pattern { get; set; } + + /// Identifier of the concrete phase that produced the event. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("phaseId")] + public string? PhaseId { get; set; } + + /// Kind of concrete phase that produced the event. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("phaseKind")] + public string? PhaseKind { get; set; } + + /// HydraFusion routing policy used for the turn. + [JsonPropertyName("policy")] + public required string Policy { get; set; } + + /// Semantic role assigned to the concrete phase. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("role")] + public string? Role { get; set; } + + /// Concrete model that produced the attributed event. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sourceModel")] + public string? SourceModel { get; set; } + + /// Phase whose output supplied the authoritative content, when different from the executing phase. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sourcePhaseId")] + public string? SourcePhaseId { get; set; } + + /// Synthetic HydraFusion model selected for the session. + [JsonPropertyName("syntheticModel")] + public required string SyntheticModel { 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)] @@ -6219,6 +7108,19 @@ public sealed partial class AssistantMessageServerTools public JsonElement[]? RawContentBlocks { get; set; } } +/// Hosted program that requested this client tool call. +/// Nested data type for AssistantMessageToolRequestCaller. +public sealed partial class AssistantMessageToolRequestCaller +{ + /// Provider-assigned identifier for the hosted caller. + [JsonPropertyName("callerId")] + public required string CallerId { get; set; } + + /// Kind of hosted caller that requested the client tool call. + [JsonPropertyName("type")] + public required AssistantMessageToolRequestCallerType Type { get; set; } +} + /// A tool invocation request from the assistant. /// Nested data type for AssistantMessageToolRequest. public sealed partial class AssistantMessageToolRequest @@ -6228,6 +7130,11 @@ public sealed partial class AssistantMessageToolRequest [JsonPropertyName("arguments")] public JsonElement? Arguments { get; set; } + /// Hosted program that requested this client tool call. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("caller")] + public AssistantMessageToolRequestCaller? Caller { get; set; } + /// Resolved intention summary describing what this specific call does. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("intentionSummary")] @@ -6752,6 +7659,11 @@ public sealed partial class ToolExecutionCompleteContentShellExit : ToolExecutio [JsonPropertyName("exitCode")] public required long ExitCode { get; set; } + /// Path reported in the shell session's filesystem namespace when shell output exceeded the configured large-output threshold. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("outputFilePath")] + public string? OutputFilePath { get; set; } + /// Output associated with this shell command, if available. May be partial, truncated, or a preview; not guaranteed to be full output. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("outputPreview")] @@ -8288,6 +9200,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)] @@ -9416,6 +10333,70 @@ public sealed partial class McpAppToolCallCompleteToolMeta public McpAppToolCallCompleteToolMetaUI? Ui { get; set; } } +/// Routing preference used when the session model is `auto`. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AutoTier : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AutoTier(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Optimize for efficiency. + public static AutoTier Efficiency { get; } = new("efficiency"); + + /// Balance efficiency and intelligence. + public static AutoTier Balance { get; } = new("balance"); + + /// Optimize for intelligence. + public static AutoTier Intelligence { get; } = new("intelligence"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AutoTier left, AutoTier right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AutoTier left, AutoTier right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AutoTier other && Equals(other); + + /// + public bool Equals(AutoTier other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public 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 AutoTier Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AutoTier value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AutoTier)); + } + } +} + /// Hosting platform type of the repository (github or ado). [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -9666,6 +10647,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 +10991,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))] @@ -10443,15 +11424,204 @@ public TaskCompletionOutcome(string value) public sealed class Converter : JsonConverter { /// - public override TaskCompletionOutcome Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskCompletionOutcome Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, TaskCompletionOutcome value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskCompletionOutcome)); + } + } +} + +/// Kind of turn for which HydraFusion routing is running. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct FusionTurnKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public FusionTurnKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A user-message turn. + public static FusionTurnKind User { get; } = new("user"); + + /// A conversation-compaction turn. + public static FusionTurnKind Compaction { get; } = new("compaction"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionTurnKind left, FusionTurnKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionTurnKind left, FusionTurnKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is FusionTurnKind other && Equals(other); + + /// + public bool Equals(FusionTurnKind 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 FusionTurnKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, FusionTurnKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionTurnKind)); + } + } +} + +/// Server-recommended routing behavior for a later HydraFusion turn. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct FusionFollowUpAction : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public FusionFollowUpAction(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Reuse the durable primary model without routing. + public static FusionFollowUpAction ReusePrimary { get; } = new("reuse_primary"); + + /// Request a new routing decision. + public static FusionFollowUpAction Reroute { get; } = new("reroute"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionFollowUpAction left, FusionFollowUpAction right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionFollowUpAction left, FusionFollowUpAction right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is FusionFollowUpAction other && Equals(other); + + /// + public bool Equals(FusionFollowUpAction 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 FusionFollowUpAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, FusionFollowUpAction value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionFollowUpAction)); + } + } +} + +/// Validated HydraFusion execution pattern. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct FusionPattern : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public FusionPattern(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Run one primary solver phase. + public static FusionPattern Single { get; } = new("single"); + + /// Run a primary phase, a judge, and an optional repair. + public static FusionPattern Cascade { get; } = new("cascade"); + + /// Run a primary draft, a read-only critique, and a revision. + public static FusionPattern Critique { get; } = new("critique"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionPattern left, FusionPattern right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionPattern left, FusionPattern right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is FusionPattern other && Equals(other); + + /// + public bool Equals(FusionPattern 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 FusionPattern Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskCompletionOutcome value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, FusionPattern value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskCompletionOutcome)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionPattern)); } } } @@ -10901,6 +12071,275 @@ public override void Write(Utf8JsonWriter writer, ModelCallFailureTransport valu } } +/// Conversation scope in which a HydraFusion phase executes. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct FusionConversationScope : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public FusionConversationScope(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Canonical root conversation history. + public static FusionConversationScope Root { get; } = new("root"); + + /// Isolated read-only review history that does not enter the root conversation. + public static FusionConversationScope Review { get; } = new("review"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionConversationScope left, FusionConversationScope right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionConversationScope left, FusionConversationScope right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is FusionConversationScope other && Equals(other); + + /// + public bool Equals(FusionConversationScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override FusionConversationScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, FusionConversationScope value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionConversationScope)); + } + } +} + +/// HydraFusion phase kind. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct FusionPhaseKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public FusionPhaseKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Primary solver phase. + public static FusionPhaseKind Primary { get; } = new("primary"); + + /// Read-only cascade judge phase. + public static FusionPhaseKind Judge { get; } = new("judge"); + + /// Cascade repair phase. + public static FusionPhaseKind Repair { get; } = new("repair"); + + /// Initial critique-pattern draft phase. + public static FusionPhaseKind Draft { get; } = new("draft"); + + /// Read-only critique phase. + public static FusionPhaseKind Critic { get; } = new("critic"); + + /// Critique-pattern revision phase. + public static FusionPhaseKind Revision { get; } = new("revision"); + + /// Follow-up phase continuing from the resolved model. + public static FusionPhaseKind FollowUp { get; } = new("follow_up"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionPhaseKind left, FusionPhaseKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionPhaseKind left, FusionPhaseKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is FusionPhaseKind other && Equals(other); + + /// + public bool Equals(FusionPhaseKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override FusionPhaseKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, FusionPhaseKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionPhaseKind)); + } + } +} + +/// How a durable phase checkpoint contributes its exact message to canonical root history. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct FusionProjectionMode : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public FusionProjectionMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Append the exact root message immediately. + public static FusionProjectionMode Append { get; } = new("append"); + + /// Hold a terminal message outside canonical history until the final commit selects it. + public static FusionProjectionMode Staged { get; } = new("staged"); + + /// Do not project the checkpoint into root history. + public static FusionProjectionMode None { get; } = new("none"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionProjectionMode left, FusionProjectionMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionProjectionMode left, FusionProjectionMode right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is FusionProjectionMode other && Equals(other); + + /// + public bool Equals(FusionProjectionMode 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 FusionProjectionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, FusionProjectionMode value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionProjectionMode)); + } + } +} + +/// Durable outcome status of a HydraFusion phase. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct FusionPhaseStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public FusionPhaseStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The phase completed successfully. + public static FusionPhaseStatus Succeeded { get; } = new("succeeded"); + + /// The phase failed. + public static FusionPhaseStatus Failed { get; } = new("failed"); + + /// The phase was cancelled. + public static FusionPhaseStatus Cancelled { get; } = new("cancelled"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionPhaseStatus left, FusionPhaseStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionPhaseStatus left, FusionPhaseStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is FusionPhaseStatus other && Equals(other); + + /// + public bool Equals(FusionPhaseStatus 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 FusionPhaseStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, FusionPhaseStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionPhaseStatus)); + } + } +} + /// Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -11027,6 +12466,64 @@ public override void Write(Utf8JsonWriter writer, CitationProvider value, JsonSe } } +/// Hosted program caller type. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AssistantMessageToolRequestCallerType : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AssistantMessageToolRequestCallerType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Gets the program value. + public static AssistantMessageToolRequestCallerType Program { get; } = new("program"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AssistantMessageToolRequestCallerType left, AssistantMessageToolRequestCallerType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AssistantMessageToolRequestCallerType left, AssistantMessageToolRequestCallerType right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AssistantMessageToolRequestCallerType other && Equals(other); + + /// + public bool Equals(AssistantMessageToolRequestCallerType 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 AssistantMessageToolRequestCallerType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AssistantMessageToolRequestCallerType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AssistantMessageToolRequestCallerType)); + } + } +} + /// API endpoint used for this model call, matching CAPI supported_endpoints vocabulary. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -11341,6 +12838,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 +14967,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); @@ -14002,6 +15569,12 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(AbortEvent))] [JsonSerializable(typeof(AgentInterruptedData))] [JsonSerializable(typeof(AgentInterruptedEvent))] +[JsonSerializable(typeof(AssistantFusionPhaseCompletedData))] +[JsonSerializable(typeof(AssistantFusionPhaseCompletedEvent))] +[JsonSerializable(typeof(AssistantFusionPhaseFailedData))] +[JsonSerializable(typeof(AssistantFusionPhaseFailedEvent))] +[JsonSerializable(typeof(AssistantFusionPhaseStartedData))] +[JsonSerializable(typeof(AssistantFusionPhaseStartedEvent))] [JsonSerializable(typeof(AssistantIdleData))] [JsonSerializable(typeof(AssistantIdleEvent))] [JsonSerializable(typeof(AssistantIntentData))] @@ -14010,10 +15583,12 @@ 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))] [JsonSerializable(typeof(AssistantMessageToolRequest))] +[JsonSerializable(typeof(AssistantMessageToolRequestCaller))] [JsonSerializable(typeof(AssistantReasoningData))] [JsonSerializable(typeof(AssistantReasoningDeltaData))] [JsonSerializable(typeof(AssistantReasoningDeltaEvent))] @@ -14112,6 +15687,11 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(FactoryRunStartedEvent))] [JsonSerializable(typeof(FactoryRunUpdatedData))] [JsonSerializable(typeof(FactoryRunUpdatedEvent))] +[JsonSerializable(typeof(FusionAttribution))] +[JsonSerializable(typeof(FusionFollowUpRecommendation))] +[JsonSerializable(typeof(FusionPhaseUsage))] +[JsonSerializable(typeof(FusionScores))] +[JsonSerializable(typeof(FusionStagedTerminal))] [JsonSerializable(typeof(GitHubMcpToolConfig))] [JsonSerializable(typeof(GitHubRepoRef))] [JsonSerializable(typeof(HandoffRepository))] @@ -14149,6 +15729,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))] @@ -14249,6 +15831,14 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SessionExtensionsAttachmentsPushedEvent))] [JsonSerializable(typeof(SessionExtensionsLoadedData))] [JsonSerializable(typeof(SessionExtensionsLoadedEvent))] +[JsonSerializable(typeof(SessionFusionCompletedData))] +[JsonSerializable(typeof(SessionFusionCompletedEvent))] +[JsonSerializable(typeof(SessionFusionResolvedData))] +[JsonSerializable(typeof(SessionFusionResolvedEvent))] +[JsonSerializable(typeof(SessionFusionRouteFailedData))] +[JsonSerializable(typeof(SessionFusionRouteFailedEvent))] +[JsonSerializable(typeof(SessionFusionRouteStartedData))] +[JsonSerializable(typeof(SessionFusionRouteStartedEvent))] [JsonSerializable(typeof(SessionHandoffData))] [JsonSerializable(typeof(SessionHandoffEvent))] [JsonSerializable(typeof(SessionIdleData))] @@ -14327,6 +15917,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SkillsLoadedSkill))] [JsonSerializable(typeof(SubagentCompletedData))] [JsonSerializable(typeof(SubagentCompletedEvent))] +[JsonSerializable(typeof(SubagentConfiguredData))] +[JsonSerializable(typeof(SubagentConfiguredEvent))] [JsonSerializable(typeof(SubagentDeselectedData))] [JsonSerializable(typeof(SubagentDeselectedEvent))] [JsonSerializable(typeof(SubagentFailedData))] 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/RewindE2ETests.cs b/dotnet/test/E2E/RewindE2ETests.cs index 75aa328abe..74c0b8ab9f 100644 --- a/dotnet/test/E2E/RewindE2ETests.cs +++ b/dotnet/test/E2E/RewindE2ETests.cs @@ -18,6 +18,10 @@ public class RewindE2ETests(E2ETestFixture fixture, ITestOutputHelper output) [Fact] public async Task Should_Restore_Tracked_File_And_Conversation() { + // TODO(cli-1.0.81): Re-enable when Windows file-change tracking records built-in create tool writes. + if (OperatingSystem.IsWindows()) + return; + var filePath = Path.Join(Ctx.WorkDir, FileName); await using var session = await CreateSessionAsync(new SessionConfig { @@ -47,7 +51,7 @@ await TestHelper.WaitForConditionAsync( && rewindPoints.Points[0].CanRestoreFiles && rewindPoints.Points[0].FileCount == 1; }, - timeout: TimeSpan.FromSeconds(10), + timeout: TimeSpan.FromSeconds(30), timeoutMessage: "Timed out waiting for a restorable file rewind point.", pollInterval: TimeSpan.FromMilliseconds(100)); 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/E2E/TelemetryExportE2ETests.cs b/dotnet/test/E2E/TelemetryExportE2ETests.cs index 48e3b53ad6..e2ad447e26 100644 --- a/dotnet/test/E2E/TelemetryExportE2ETests.cs +++ b/dotnet/test/E2E/TelemetryExportE2ETests.cs @@ -54,9 +54,6 @@ public async Task Should_Export_File_Telemetry_For_Sdk_Interactions() Assert.NotEmpty(spans); Assert.All(spans, span => Assert.Equal(sourceName, GetInstrumentationScopeName(span))); - // All spans for one SDK turn must share the same trace id and must not be in error state. - var traceIds = spans.Select(GetTraceId).Where(id => !string.IsNullOrEmpty(id)).Distinct().ToList(); - Assert.Single(traceIds); Assert.All(spans, span => Assert.NotEqual(2, GetStatusCode(span))); var invokeAgentSpan = AssertSpanWithOperation(spans, "invoke_agent"); @@ -65,10 +62,13 @@ public async Task Should_Export_File_Telemetry_For_Sdk_Interactions() "invoke_agent should be the root of the SDK turn trace."); var invokeAgentSpanId = GetSpanId(invokeAgentSpan); Assert.False(string.IsNullOrEmpty(invokeAgentSpanId)); + var invokeAgentTraceId = GetTraceId(invokeAgentSpan); + Assert.False(string.IsNullOrEmpty(invokeAgentTraceId)); var chatSpans = spans.Where(span => IsSpanWithOperation(span, "chat")).ToList(); Assert.NotEmpty(chatSpans); Assert.All(chatSpans, chat => Assert.Equal(invokeAgentSpanId, GetParentSpanId(chat))); + Assert.All(chatSpans, chat => Assert.Equal(invokeAgentTraceId, GetTraceId(chat))); Assert.Contains( chatSpans, span => (GetStringAttribute(span, "gen_ai.input.messages") ?? string.Empty).Contains(prompt, StringComparison.Ordinal)); @@ -78,6 +78,7 @@ public async Task Should_Export_File_Telemetry_For_Sdk_Interactions() var toolSpan = AssertSpanWithOperation(spans, "execute_tool"); Assert.Equal(invokeAgentSpanId, GetParentSpanId(toolSpan)); + Assert.Equal(invokeAgentTraceId, GetTraceId(toolSpan)); Assert.Equal(toolName, GetStringAttribute(toolSpan, "gen_ai.tool.name")); Assert.False(string.IsNullOrWhiteSpace(GetStringAttribute(toolSpan, "gen_ai.tool.call.id")), "execute_tool span should carry gen_ai.tool.call.id."); 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/rewind_e2e_test.go b/go/internal/e2e/rewind_e2e_test.go index b15e546eb1..5fc29a13e8 100644 --- a/go/internal/e2e/rewind_e2e_test.go +++ b/go/internal/e2e/rewind_e2e_test.go @@ -24,6 +24,10 @@ func TestRewindE2E(t *testing.T) { t.Cleanup(func() { client.ForceStop() }) t.Run("should restore tracked file and conversation", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("blocked on CLI 1.0.81 file-change tracking regression on Windows") + } + ctx.ConfigureForTest(t) filePath := filepath.Join(ctx.WorkDir, rewindFileName) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ @@ -113,17 +117,20 @@ func TestRewindE2E(t *testing.T) { func waitForRewindPoints(t *testing.T, session *copilot.Session) *rpc.HistoryListRewindPointsResult { t.Helper() - deadline := time.Now().Add(10 * time.Second) + deadline := time.Now().Add(30 * time.Second) for { result, err := session.RPC.History.ListRewindPoints(t.Context()) if err != nil { t.Fatalf("ListRewindPoints failed: %v", err) } - if result.UnavailableReason == nil { + if result.UnavailableReason == nil && + len(result.Points) == 1 && + result.Points[0].CanRestoreFiles && + result.Points[0].FileCount == 1 { return result } if time.Now().After(deadline) { - t.Fatalf("Timed out waiting for rewind points: %s", *result.UnavailableReason) + t.Fatalf("Timed out waiting for a restorable rewind point: %+v", result) } time.Sleep(100 * time.Millisecond) } 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/internal/e2e/telemetry_e2e_test.go b/go/internal/e2e/telemetry_e2e_test.go index 4567817fd9..77f8bec8ea 100644 --- a/go/internal/e2e/telemetry_e2e_test.go +++ b/go/internal/e2e/telemetry_e2e_test.go @@ -96,17 +96,6 @@ func TestTelemetryE2E(t *testing.T) { } } - traceIDs := map[string]struct{}{} - for _, span := range spans { - id := stringProp(span, "traceId") - if id != "" { - traceIDs[id] = struct{}{} - } - } - if len(traceIDs) != 1 { - t.Errorf("Expected exactly 1 trace id across spans, got %d (%v)", len(traceIDs), traceIDs) - } - invokeAgent := findSpanWithOperation(spans, "invoke_agent") if invokeAgent == nil { t.Fatal("Expected an invoke_agent span") @@ -121,6 +110,10 @@ func TestTelemetryE2E(t *testing.T) { if invokeAgentSpanID == "" { t.Fatal("invoke_agent span has empty spanId") } + invokeAgentTraceID := stringProp(invokeAgent, "traceId") + if invokeAgentTraceID == "" { + t.Fatal("invoke_agent span has empty traceId") + } var chatSpans []map[string]any for _, span := range spans { @@ -135,6 +128,9 @@ func TestTelemetryE2E(t *testing.T) { if got := stringProp(chat, "parentSpanId"); got != invokeAgentSpanID { t.Errorf("Expected chat span parentSpanId=%q, got %q", invokeAgentSpanID, got) } + if got := stringProp(chat, "traceId"); got != invokeAgentTraceID { + t.Errorf("Expected chat span traceId=%q, got %q", invokeAgentTraceID, got) + } } var sawPromptInput, sawDoneOutput bool for _, chat := range chatSpans { @@ -159,6 +155,9 @@ func TestTelemetryE2E(t *testing.T) { if got := stringProp(toolSpan, "parentSpanId"); got != invokeAgentSpanID { t.Errorf("Expected execute_tool parentSpanId=%q, got %q", invokeAgentSpanID, got) } + if got := stringProp(toolSpan, "traceId"); got != invokeAgentTraceID { + t.Errorf("Expected execute_tool traceId=%q, got %q", invokeAgentTraceID, got) + } if got := stringAttr(toolSpan, "gen_ai.tool.name"); got != toolName { t.Errorf("Expected gen_ai.tool.name=%q, got %q", toolName, got) } 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..706f32bf5f 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. @@ -1338,6 +1361,10 @@ type CanvasSessionContext struct { // Experimental: CapiSessionOptions is part of an experimental API and may change or be // removed. type CapiSessionOptions struct { + // Routing preference used when the session model is `auto`. The runtime persists the + // preference across cold resume. When omitted, the default routing behavior is used. + // Resuming an already-resident session cannot change its preference. + AutoTier *AutoTier `json:"autoTier,omitempty"` // Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when // the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses // transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting @@ -1949,6 +1976,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 +2048,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. @@ -2553,6 +2603,8 @@ type EnqueueCommandParams struct { // Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO // with any in-flight items; if the session is idle, processing kicks off immediately. Command string `json:"command"` + // Optional user-facing text for the queue row. The command string is shown when omitted. + DisplayText *string `json:"displayText,omitempty"` } // Indicates whether the command was accepted into the local execution queue. @@ -2934,6 +2986,9 @@ type ExternalToolTextResultForLlmContentShellExit struct { Cwd *string `json:"cwd,omitempty"` // Exit code from the completed shell command ExitCode int64 `json:"exitCode"` + // Path reported in the shell session's filesystem namespace when shell output exceeded the + // configured large-output threshold. + OutputFilePath *string `json:"outputFilePath,omitempty"` // Output associated with this shell command, if available. May be partial, truncated, or a // preview; not guaranteed to be full output. OutputPreview *string `json:"outputPreview,omitempty"` @@ -3370,6 +3425,10 @@ type FactoryProgressPage struct { type FactoryResumeRequest struct { // Optional per-invocation resource ceiling overrides. Limits *FactoryRunLimits `json:"limits,omitempty"` + // Whether to emit factory phase names to the session transcript. + LogPhaseNames *bool `json:"logPhaseNames,omitempty"` + // Whether to notify the originating session when the factory completes. + NotifyOnComplete *bool `json:"notifyOnComplete,omitempty"` // Factory run identifier. RunID string `json:"runId"` } @@ -3624,6 +3683,48 @@ type FactoryRunTerminal struct { ResultPreview *string `json:"resultPreview,omitempty"` } +// Internal parameters for resuming a factory run from a tool. +// Experimental: FactoryToolResumeRequest is part of an experimental API and may change or +// be removed. +// Internal: FactoryToolResumeRequest is an internal SDK API and is not part of the public +// surface. +type FactoryToolResumeRequest struct { + // Optional per-invocation resource ceiling overrides. + Limits *FactoryRunLimits `json:"limits,omitempty"` + // Factory run identifier. + RunID string `json:"runId"` + // Opaque identifier of the originating tool call. + ToolCallID *string `json:"toolCallId,omitempty"` +} + +// Options for an internal tool-originated factory invocation. +// Experimental: FactoryToolRunOptions is part of an experimental API and may change or be +// removed. +// Internal: FactoryToolRunOptions is an internal SDK API and is not part of the public +// surface. +type FactoryToolRunOptions struct { + // Per-invocation resource ceiling overrides. + Limits *FactoryRunLimits `json:"limits,omitempty"` + // Run identifier whose journal and progress should seed this resumed run. + ResumeFromRunID *string `json:"resumeFromRunId,omitempty"` +} + +// Internal parameters for invoking a registered factory from a tool. +// Experimental: FactoryToolRunRequest is part of an experimental API and may change or be +// removed. +// Internal: FactoryToolRunRequest is an internal SDK API and is not part of the public +// surface. +type FactoryToolRunRequest struct { + // Factory input value. + Args any `json:"args"` + // Registered factory name. + Name string `json:"name"` + // Tool-originated factory invocation options. + Options *FactoryToolRunOptions `json:"options,omitempty"` + // Opaque identifier of the originating tool call. + ToolCallID *string `json:"toolCallId,omitempty"` +} + // Content filtering mode to apply to all tools, or a map of tool name to content filtering // mode. // Experimental: FilterMapping is part of an experimental API and may change or be removed. @@ -3700,6 +3801,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 +3870,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 +4234,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 +4266,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 +5430,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 +6842,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 +6861,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 +7090,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 +7246,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 +8053,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 +8134,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 +8163,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 +9801,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"` @@ -9985,6 +10201,10 @@ type RemoteSessionRepository struct { type RunOptions struct { // Per-invocation resource ceiling overrides. Limits *FactoryRunLimits `json:"limits,omitempty"` + // Whether to emit factory phase names to the session transcript. + LogPhaseNames *bool `json:"logPhaseNames,omitempty"` + // Whether to notify the originating session when the factory completes. + NotifyOnComplete *bool `json:"notifyOnComplete,omitempty"` // Run identifier whose journal and progress should seed this resumed run. ResumeFromRunID *string `json:"resumeFromRunId,omitempty"` } @@ -9999,20 +10219,16 @@ type RuntimeShutdownResult struct { type SandboxConfig struct { // Whether to auto-add the current working directory to readwritePaths. Default: true. AddCurrentWorkingDirectory *bool `json:"addCurrentWorkingDirectory,omitempty"` - // 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 + // Whether to auto-grant read access to tool directories discovered on PATH and in toolchain + // environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common + // developer-tool caches, config, and toolchains. Writable grants cover scratch caches, the + // Unix GitHub CLI cache, and Cargo's registry, git store, and lock/tracker files. A + // relocated CARGO_HOME gets the same narrow split: registry and git are read-write; bin is + // read-only; the home root, config.toml, and credentials.toml stay ungranted. Set to false + // to disable every grant listed above; user-installed toolchains and caches then need + // explicit userPolicy.filesystem readonlyPaths and readwritePaths entries. The working + // directory (see addCurrentWorkingDirectory), temporary storage, session log paths, and + // system locations follow their own rules and stay granted. Default: true (enabled by // default; set to false to opt out). AllowDevToolAccess *bool `json:"allowDevToolAccess,omitempty"` // Credential-injection capability flags. @@ -10133,6 +10349,18 @@ type SandboxConfigUserPolicySeatbelt struct { KeychainAccess *bool `json:"keychainAccess,omitempty"` } +// Managed sandbox enforcement state for a session. +// Experimental: SandboxEnforcementStatus is part of an experimental API and may change or +// be removed. +type SandboxEnforcementStatus struct { + // Whether an enforcement failure has permanently blocked the session. + Blocked bool `json:"blocked"` + // The first sandbox enforcement failure that blocked the session. + Reason *string `json:"reason,omitempty"` + // Whether the effective managed policy requires an available sandbox backend. + Required bool `json:"required"` +} + // Register an absolute-time scheduled prompt. // Experimental: ScheduleAddAtRequest is part of an experimental API and may change or be // removed. @@ -10533,6 +10761,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 +11418,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 +11711,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 +11877,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 +11984,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 +12038,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 +12473,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 +13091,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 +13133,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 +13211,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. @@ -14026,12 +14351,8 @@ type ToolsGetBuiltinDescriptorsRequest struct { BackgroundTaskNotificationsEnabled *bool `json:"backgroundTaskNotificationsEnabled,omitempty"` // Whether tool descriptors should include authoring metadata. IncludeAuthor *bool `json:"includeAuthor,omitempty"` - // Whether line numbers should be omitted from the view tool descriptor. - NoViewLineNumbers *bool `json:"noViewLineNumbers,omitempty"` // Whether descriptors should favor fewer user-intervention prompts. ReduceUserIntervention *bool `json:"reduceUserIntervention,omitempty"` - // Whether shell commands may only run asynchronously. - ShellAsyncOnlyEnabled *bool `json:"shellAsyncOnlyEnabled,omitempty"` // Shell-specific names and description lines for shell tools. ShellConfig *ToolsShellDescriptorConfig `json:"shellConfig,omitempty"` // Whether the configured shell supports PowerShell 7 syntax. @@ -15551,9 +15872,23 @@ const ( AuthInfoTypeGhCLI AuthInfoType = "gh-cli" AuthInfoTypeHMAC AuthInfoType = "hmac" AuthInfoTypeToken AuthInfoType = "token" + AuthInfoTypeTokenProvider AuthInfoType = "token-provider" AuthInfoTypeUser AuthInfoType = "user" ) +// Routing preference used when the session model is `auto`. +// Experimental: AutoTier is part of an experimental API and may change or be removed. +type AutoTier string + +const ( + // Balance efficiency and intelligence. + AutoTierBalance AutoTier = "balance" + // Optimize for efficiency. + AutoTierEfficiency AutoTier = "efficiency" + // Optimize for intelligence. + AutoTierIntelligence AutoTier = "intelligence" +) + // Custom input-format kind. // Experimental: BuiltinToolFormatType is part of an experimental API and may change or be // removed. @@ -16018,14 +16353,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 +16623,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 +17041,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 +17758,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 +17815,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 +18096,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 +18579,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 @@ -20879,6 +21281,9 @@ func (a *CommandsAPI) Enqueue(ctx context.Context, params *EnqueueCommandParams) req := map[string]any{"sessionId": a.sessionID} if params != nil { req["command"] = params.Command + if params.DisplayText != nil { + req["displayText"] = *params.DisplayText + } } raw, err := a.client.Request(ctx, "session.commands.enqueue", req) if err != nil { @@ -21577,6 +21982,12 @@ func (a *FactoryAPI) Resume(ctx context.Context, params *FactoryResumeRequest) ( if params.Limits != nil { req["limits"] = *params.Limits } + if params.LogPhaseNames != nil { + req["logPhaseNames"] = *params.LogPhaseNames + } + if params.NotifyOnComplete != nil { + req["notifyOnComplete"] = *params.NotifyOnComplete + } req["runId"] = params.RunID } raw, err := a.client.Request(ctx, "session.factory.resume", req) @@ -23456,6 +23867,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 +23915,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 +24390,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. // @@ -24653,6 +25071,28 @@ func (a *RemoteAPI) NotifySteerableChanged(ctx context.Context, params *RemoteNo return &result, nil } +// Experimental: SandboxAPI contains experimental APIs that may change or be removed. +type SandboxAPI sessionAPI + +// GetEnforcementStatus returns whether managed policy requires sandbox enforcement and +// whether an enforcement failure has permanently blocked the session. +// +// RPC method: session.sandbox.getEnforcementStatus. +// +// Returns: Managed sandbox enforcement state for a session. +func (a *SandboxAPI) GetEnforcementStatus(ctx context.Context) (*SandboxEnforcementStatus, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.sandbox.getEnforcementStatus", req) + if err != nil { + return nil, err + } + var result SandboxEnforcementStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: ScheduleAPI contains experimental APIs that may change or be removed. type ScheduleAPI sessionAPI @@ -25284,15 +25724,9 @@ func (a *ToolsAPI) GetBuiltinDescriptors(ctx context.Context, params *ToolsGetBu if params.IncludeAuthor != nil { req["includeAuthor"] = *params.IncludeAuthor } - if params.NoViewLineNumbers != nil { - req["noViewLineNumbers"] = *params.NoViewLineNumbers - } if params.ReduceUserIntervention != nil { req["reduceUserIntervention"] = *params.ReduceUserIntervention } - if params.ShellAsyncOnlyEnabled != nil { - req["shellAsyncOnlyEnabled"] = *params.ShellAsyncOnlyEnabled - } if params.ShellConfig != nil { req["shellConfig"] = *params.ShellConfig } @@ -26208,6 +26642,7 @@ type SessionRPC struct { Provider *ProviderAPI Queue *QueueAPI Remote *RemoteAPI + Sandbox *SandboxAPI Schedule *ScheduleAPI Shell *ShellAPI Skills *SkillsAPI @@ -26529,6 +26964,7 @@ func NewSessionRPC(client *jsonrpc2.Client, sessionID string) *SessionRPC { r.Provider = (*ProviderAPI)(&r.common) r.Queue = (*QueueAPI)(&r.common) r.Remote = (*RemoteAPI)(&r.common) + r.Sandbox = (*SandboxAPI)(&r.common) r.Schedule = (*ScheduleAPI)(&r.common) r.Shell = (*ShellAPI)(&r.common) r.Skills = (*SkillsAPI)(&r.common) @@ -26640,6 +27076,72 @@ func (a *InternalCommandsAPI) FinalizeInvocationEffect(ctx context.Context, para return &result, nil } +// Experimental: InternalFactoryAPI contains experimental APIs that may change or be removed. +type InternalFactoryAPI internalSessionAPI + +// ResumeFromTool internal tool-originated factory resume. +// +// RPC method: session.factory.resumeFromTool. +// +// Parameters: Internal parameters for resuming a factory run from a tool. +// +// Returns: Resolved persisted factory identity and resumed run envelope. +// Internal: ResumeFromTool is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalFactoryAPI) ResumeFromTool(ctx context.Context, params *FactoryToolResumeRequest) (*FactoryResumeResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + if params.Limits != nil { + req["limits"] = *params.Limits + } + req["runId"] = params.RunID + if params.ToolCallID != nil { + req["toolCallId"] = *params.ToolCallID + } + } + raw, err := a.client.Request(ctx, "session.factory.resumeFromTool", req) + if err != nil { + return nil, err + } + var result FactoryResumeResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// RunFromTool internal tool-originated factory invocation. +// +// RPC method: session.factory.runFromTool. +// +// Parameters: Internal parameters for invoking a registered factory from a tool. +// +// Returns: Complete current or terminal factory run envelope. +// Internal: RunFromTool is part of the SDK's internal handshake/plumbing; external callers +// should not use it. +func (a *InternalFactoryAPI) RunFromTool(ctx context.Context, params *FactoryToolRunRequest) (*FactoryRunResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["args"] = params.Args + req["name"] = params.Name + if params.Options != nil { + req["options"] = *params.Options + } + if params.ToolCallID != nil { + req["toolCallId"] = *params.ToolCallID + } + } + raw, err := a.client.Request(ctx, "session.factory.runFromTool", req) + if err != nil { + return nil, err + } + var result FactoryRunResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: InternalGitHubAuthAPI contains experimental APIs that may change or be // removed. type InternalGitHubAuthAPI internalSessionAPI @@ -27438,6 +27940,7 @@ type InternalSessionRPC struct { Canvas *InternalCanvasAPI Commands *InternalCommandsAPI + Factory *InternalFactoryAPI GitHubAuth *InternalGitHubAuthAPI MCP *InternalMCPAPI Model *InternalModelAPI @@ -27483,6 +27986,7 @@ func NewInternalSessionRPC(client *jsonrpc2.Client, sessionID string) *InternalS r.common = internalSessionAPI{client: client, sessionID: sessionID} r.Canvas = (*InternalCanvasAPI)(&r.common) r.Commands = (*InternalCommandsAPI)(&r.common) + r.Factory = (*InternalFactoryAPI)(&r.common) r.GitHubAuth = (*InternalGitHubAuthAPI)(&r.common) r.MCP = (*InternalMCPAPI)(&r.common) r.Model = (*InternalModelAPI)(&r.common) @@ -28107,6 +28611,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 +28678,7 @@ type LlmInferenceHandler interface { type ClientGlobalAPIHandlers struct { ExtensionLaunchProvider ExtensionLaunchProviderHandler GitHubTelemetry GitHubTelemetryHandler + GitHubToken GitHubTokenHandler Hooks HooksHandler LlmInference LlmInferenceHandler } @@ -28208,6 +28728,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..4c03a42c00 100644 --- a/go/rpc/zsession_encoding.go +++ b/go/rpc/zsession_encoding.go @@ -47,6 +47,24 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeAssistantFusionPhaseCompleted: + var d AssistantFusionPhaseCompletedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d + case SessionEventTypeAssistantFusionPhaseFailed: + var d AssistantFusionPhaseFailedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d + case SessionEventTypeAssistantFusionPhaseStarted: + var d AssistantFusionPhaseStartedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeAssistantIdle: var d AssistantIdleData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -299,6 +317,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 { @@ -461,6 +485,30 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeSessionFusionCompleted: + var d SessionFusionCompletedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d + case SessionEventTypeSessionFusionResolved: + var d SessionFusionResolvedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d + case SessionEventTypeSessionFusionRouteFailed: + var d SessionFusionRouteFailedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d + case SessionEventTypeSessionFusionRouteStarted: + var d SessionFusionRouteStartedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeSessionHandoff: var d SessionHandoffData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -665,6 +713,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeSubagentConfigured: + var d SubagentConfiguredData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeSubagentDeselected: var d SubagentDeselectedData 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..07504e8815 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -53,8 +53,17 @@ func (r RawSessionEventData) Type() SessionEventType { type SessionEventType string const ( - SessionEventTypeAbort SessionEventType = "abort" - SessionEventTypeAgentInterrupted SessionEventType = "agent.interrupted" + SessionEventTypeAbort SessionEventType = "abort" + SessionEventTypeAgentInterrupted SessionEventType = "agent.interrupted" + // Experimental: SessionEventTypeAssistantFusionPhaseCompleted identifies an experimental + // event that may change or be removed. + SessionEventTypeAssistantFusionPhaseCompleted SessionEventType = "assistant.fusion_phase_completed" + // Experimental: SessionEventTypeAssistantFusionPhaseFailed identifies an experimental event + // that may change or be removed. + SessionEventTypeAssistantFusionPhaseFailed SessionEventType = "assistant.fusion_phase_failed" + // Experimental: SessionEventTypeAssistantFusionPhaseStarted identifies an experimental + // event that may change or be removed. + SessionEventTypeAssistantFusionPhaseStarted SessionEventType = "assistant.fusion_phase_started" SessionEventTypeAssistantIdle SessionEventType = "assistant.idle" SessionEventTypeAssistantIntent SessionEventType = "assistant.intent" SessionEventTypeAssistantMessage SessionEventType = "assistant.message" @@ -103,6 +112,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" @@ -146,11 +156,23 @@ const ( SessionEventTypeSessionError SessionEventType = "session.error" SessionEventTypeSessionExtensionsAttachmentsPushed SessionEventType = "session.extensions.attachments_pushed" SessionEventTypeSessionExtensionsLoaded SessionEventType = "session.extensions_loaded" - SessionEventTypeSessionHandoff SessionEventType = "session.handoff" - SessionEventTypeSessionIdle SessionEventType = "session.idle" - SessionEventTypeSessionInfo SessionEventType = "session.info" - SessionEventTypeSessionLimitsExhaustedCompleted SessionEventType = "session_limits_exhausted.completed" - SessionEventTypeSessionLimitsExhaustedRequested SessionEventType = "session_limits_exhausted.requested" + // Experimental: SessionEventTypeSessionFusionCompleted identifies an experimental event + // that may change or be removed. + SessionEventTypeSessionFusionCompleted SessionEventType = "session.fusion_completed" + // Experimental: SessionEventTypeSessionFusionResolved identifies an experimental event that + // may change or be removed. + SessionEventTypeSessionFusionResolved SessionEventType = "session.fusion_resolved" + // Experimental: SessionEventTypeSessionFusionRouteFailed identifies an experimental event + // that may change or be removed. + SessionEventTypeSessionFusionRouteFailed SessionEventType = "session.fusion_route_failed" + // Experimental: SessionEventTypeSessionFusionRouteStarted identifies an experimental event + // that may change or be removed. + SessionEventTypeSessionFusionRouteStarted SessionEventType = "session.fusion_route_started" + SessionEventTypeSessionHandoff SessionEventType = "session.handoff" + SessionEventTypeSessionIdle SessionEventType = "session.idle" + SessionEventTypeSessionInfo SessionEventType = "session.info" + SessionEventTypeSessionLimitsExhaustedCompleted SessionEventType = "session_limits_exhausted.completed" + SessionEventTypeSessionLimitsExhaustedRequested SessionEventType = "session_limits_exhausted.requested" // Experimental: SessionEventTypeSessionManagedSettingsEnforced identifies an experimental // event that may change or be removed. SessionEventTypeSessionManagedSettingsEnforced SessionEventType = "session.managed_settings_enforced" @@ -186,6 +208,7 @@ const ( SessionEventTypeSessionWorkspaceFileChanged SessionEventType = "session.workspace_file_changed" SessionEventTypeSkillInvoked SessionEventType = "skill.invoked" SessionEventTypeSubagentCompleted SessionEventType = "subagent.completed" + SessionEventTypeSubagentConfigured SessionEventType = "subagent.configured" SessionEventTypeSubagentDeselected SessionEventType = "subagent.deselected" SessionEventTypeSubagentFailed SessionEventType = "subagent.failed" SessionEventTypeSubagentSelected SessionEventType = "subagent.selected" @@ -326,6 +349,9 @@ type AssistantMessageData struct { Content string `json:"content"` // Encrypted reasoning content from OpenAI models. Session-bound and stripped on resume. EncryptedContent *string `json:"encryptedContent,omitempty"` + // Experimental HydraFusion source attribution for this ordinary authoritative assistant message. + // Experimental: Fusion is part of an experimental API and may change or be removed. + Fusion *FusionAttribution `json:"fusion,omitempty"` // CAPI interaction ID for correlating this message with upstream telemetry InteractionID *string `json:"interactionId,omitempty"` // Unique identifier for this assistant message @@ -339,6 +365,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 @@ -504,6 +532,8 @@ func (*SessionContextClearedData) Type() SessionEventType { // Conversation compaction results including success status, metrics, and optional error details type SessionCompactionCompleteData struct { + // Canonical model identifier used for model-specific behavior when replaying compaction + BehaviorModelID *string `json:"behaviorModelId,omitempty"` // Checkpoint snapshot number created for recovery CheckpointNumber *int64 `json:"checkpointNumber,omitempty"` // File path where the checkpoint was stored @@ -753,6 +783,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. @@ -848,6 +880,243 @@ type SessionErrorData struct { func (*SessionErrorData) sessionEventData() {} func (*SessionErrorData) Type() SessionEventType { return SessionEventTypeSessionError } +// Experimental durable HydraFusion phase output and lossless replay checkpoint. +// Experimental: AssistantFusionPhaseCompletedData is part of an experimental API and may change or be removed. +type AssistantFusionPhaseCompletedData struct { + // Provider-normalized textual output produced by the phase. + Content string `json:"content"` + // Conversation scope in which the phase executed. + ConversationScope FusionConversationScope `json:"conversationScope"` + // Elapsed execution time for the phase in milliseconds. + DurationMs float64 `json:"durationMs"` + // Identifier of the HydraFusion turn containing the phase. + FusionID string `json:"fusionId"` + // Concrete model that executed the phase. + Model string `json:"model"` + // Stable identifier for the completed phase. + PhaseID string `json:"phaseId"` + // Kind of phase that completed. + PhaseKind FusionPhaseKind `json:"phaseKind"` + // Exact provider-normalized message used to reconstruct canonical model history. + // Internal: ProjectionMessage is part of the SDK's internal API surface and is not intended for external use. + ProjectionMessage any `json:"projectionMessage,omitempty"` + // Projection action for the exact internal message. + // Internal: ProjectionMode is part of the SDK's internal API surface and is not intended for external use. + ProjectionMode *FusionProjectionMode `json:"projectionMode,omitempty"` + // Semantic role assigned to the completed phase. + Role string `json:"role"` + // Terminal request held outside canonical state until selected by the final commit. + // Internal: StagedTerminal is part of the SDK's internal API surface and is not intended for external use. + StagedTerminal *FusionStagedTerminal `json:"stagedTerminal,omitempty"` + // Durable outcome status of the phase. + Status FusionPhaseStatus `json:"status"` + // Aggregate concrete-model usage consumed by the phase. + Usage FusionPhaseUsage `json:"usage"` + // Structured judge or critic verdict, when the phase produces one. + Verdict *string `json:"verdict"` +} + +func (*AssistantFusionPhaseCompletedData) sessionEventData() {} +func (*AssistantFusionPhaseCompletedData) Type() SessionEventType { + return SessionEventTypeAssistantFusionPhaseCompleted +} + +// Experimental durable HydraFusion routing failure and the deterministic concrete fallback selected for the turn. +// Experimental: SessionFusionRouteFailedData is part of an experimental API and may change or be removed. +type SessionFusionRouteFailedData struct { + // Identifier of the routing attempt that failed. + AttemptID string `json:"attemptId"` + // Provider or validation error detail, when available. + ErrorMessage *string `json:"errorMessage,omitempty"` + // Concrete model selected as the deterministic fallback. + FallbackModel string `json:"fallbackModel"` + // HydraFusion routing policy requested for the turn. + Policy string `json:"policy"` + // Stable machine-readable reason for the routing failure. + Reason string `json:"reason"` + // Elapsed routing time in milliseconds before the failure. + RoutingLatencyMs *float64 `json:"routingLatencyMs,omitempty"` + // Synthetic HydraFusion model selected for the session. + SyntheticModel string `json:"syntheticModel"` +} + +func (*SessionFusionRouteFailedData) sessionEventData() {} +func (*SessionFusionRouteFailedData) Type() SessionEventType { + return SessionEventTypeSessionFusionRouteFailed +} + +// Experimental durable aggregate outcome of a HydraFusion turn. +// Experimental: SessionFusionCompletedData is part of an experimental API and may change or be removed. +type SessionFusionCompletedData struct { + // Total cached input tokens reported across all phases. + CachedTokens int64 `json:"cachedTokens"` + // Total tokens written to prompt cache across all phases. + CacheWriteTokens *int64 `json:"cacheWriteTokens,omitempty"` + // Idempotency identifier for the authoritative final commit. + CommitID string `json:"commitId"` + // Reason the turn used a degraded route, when applicable. + DegradedReason *string `json:"degradedReason"` + // Total elapsed execution time for the HydraFusion turn in milliseconds. + DurationMs float64 `json:"durationMs"` + // Concrete model that supplied the authoritative final content. + FinalSourceModel *string `json:"finalSourceModel"` + // Phase whose output supplied the authoritative final content. + FinalSourcePhaseID *string `json:"finalSourcePhaseId"` + // Concrete model recommended for eligible follow-up turns. + FollowUpModel string `json:"followUpModel"` + // Stable identifier for the completed HydraFusion turn. + FusionID string `json:"fusionId"` + // Total input tokens consumed across all phases. + InputTokens int64 `json:"inputTokens"` + // Stable aggregate outcome of the HydraFusion turn. + Outcome string `json:"outcome"` + // Total output tokens produced across all phases. + OutputTokens int64 `json:"outputTokens"` + // HydraFusion orchestration pattern executed for the turn. + Pattern FusionPattern `json:"pattern"` + // Number of concrete phases attempted by the turn. + PhaseCount int64 `json:"phaseCount"` + // Total concrete model requests made across all phases. + RequestCount int64 `json:"requestCount"` + // Synthetic HydraFusion model selected for the session. + SyntheticModel string `json:"syntheticModel"` + // Total normalized AI-unit cost reported across all phases, in nano-AIU. + TotalNanoAiu float64 `json:"totalNanoAiu"` + // Identifier of the session turn associated with the completion. + TurnID string `json:"turnId"` +} + +func (*SessionFusionCompletedData) sessionEventData() {} +func (*SessionFusionCompletedData) Type() SessionEventType { + return SessionEventTypeSessionFusionCompleted +} + +// Experimental durable typed HydraFusion phase failure and degradation transition. +// Experimental: AssistantFusionPhaseFailedData is part of an experimental API and may change or be removed. +type AssistantFusionPhaseFailedData struct { + // Conversation scope in which the phase executed. + ConversationScope FusionConversationScope `json:"conversationScope"` + // Identifier of the fallback phase used to continue the turn after degradation. + DegradedToPhaseID *string `json:"degradedToPhaseId,omitempty"` + // Elapsed execution time before the phase failed, in milliseconds. + DurationMs float64 `json:"durationMs"` + // Provider or execution error detail, when available. + ErrorMessage *string `json:"errorMessage,omitempty"` + // Identifier of the HydraFusion turn containing the phase. + FusionID string `json:"fusionId"` + // Concrete model that attempted the phase. + Model string `json:"model"` + // Stable identifier for the failed phase. + PhaseID string `json:"phaseId"` + // Kind of phase that failed. + PhaseKind FusionPhaseKind `json:"phaseKind"` + // Stable machine-readable reason for the phase failure. + Reason string `json:"reason"` + // Semantic role assigned to the failed phase. + Role string `json:"role"` + // Durable outcome status of the phase. + Status FusionPhaseStatus `json:"status"` + // Aggregate concrete-model usage consumed before the failure. + Usage FusionPhaseUsage `json:"usage"` +} + +func (*AssistantFusionPhaseFailedData) sessionEventData() {} +func (*AssistantFusionPhaseFailedData) Type() SessionEventType { + return SessionEventTypeAssistantFusionPhaseFailed +} + +// Experimental durable validated HydraFusion route and turn policy. +// Experimental: SessionFusionResolvedData is part of an experimental API and may change or be removed. +type SessionFusionResolvedData struct { + // Version of the validated HydraFusion event contract. + ContractVersion int64 `json:"contractVersion"` + // Concrete model used when the planned primary model cannot execute. + FallbackModel string `json:"fallbackModel"` + // Router recommendation controlling reuse or rerouting on later turns. + FollowUp *FusionFollowUpRecommendation `json:"followUp,omitempty"` + // Concrete model recommended for eligible follow-up turns. + FollowUpModel string `json:"followUpModel"` + // Stable identifier for the resolved HydraFusion turn. + FusionID string `json:"fusionId"` + // Version of the executable model universe used for selection. + ModelUniverseVersion *string `json:"modelUniverseVersion,omitempty"` + // Validated orchestration pattern selected for the turn. + Pattern FusionPattern `json:"pattern"` + // Version of the validated execution-plan format. + PlanVersion *string `json:"planVersion,omitempty"` + // HydraFusion routing policy used to resolve the plan. + Policy string `json:"policy"` + // Version of the local routing policy. + PolicyVersion *string `json:"policyVersion,omitempty"` + // Concrete model selected for the primary solver phase. + PrimaryModel string `json:"primaryModel"` + // Router implementation that supplied the plan. + RouteSource *string `json:"routeSource,omitempty"` + // Elapsed time in milliseconds required to resolve and validate the route. + RoutingLatencyMs *float64 `json:"routingLatencyMs,omitempty"` + // Identifier of the local policy rule that matched. + RuleID *string `json:"ruleId,omitempty"` + // Zero-based index of the local policy rule that matched. + RuleIndex *int64 `json:"ruleIndex,omitempty"` + // Human-readable name of the local policy rule that matched. + RuleName *string `json:"ruleName,omitempty"` + // Validated capability scores used to select the route. + Scores *FusionScores `json:"scores,omitempty"` + // Concrete model selected for the review or judge phase, when required. + SecondaryModel *string `json:"secondaryModel"` + // Synthetic HydraFusion model selected for the session. + SyntheticModel string `json:"syntheticModel"` + // Identifier of the session turn associated with the route. + TurnID string `json:"turnId"` +} + +func (*SessionFusionResolvedData) sessionEventData() {} +func (*SessionFusionResolvedData) Type() SessionEventType { + return SessionEventTypeSessionFusionResolved +} + +// Experimental transient HydraFusion phase/model/role signal. +// Experimental: AssistantFusionPhaseStartedData is part of an experimental API and may change or be removed. +type AssistantFusionPhaseStartedData struct { + // Conversation scope in which the phase executes. + ConversationScope FusionConversationScope `json:"conversationScope"` + // Identifier of the HydraFusion turn containing the phase. + FusionID string `json:"fusionId"` + // Concrete model executing the phase. + Model string `json:"model"` + // HydraFusion orchestration pattern containing the phase. + Pattern FusionPattern `json:"pattern"` + // Stable identifier for the concrete phase. + PhaseID string `json:"phaseId"` + // Kind of phase being executed. + PhaseKind FusionPhaseKind `json:"phaseKind"` + // Semantic role assigned to the phase. + Role string `json:"role"` +} + +func (*AssistantFusionPhaseStartedData) sessionEventData() {} +func (*AssistantFusionPhaseStartedData) Type() SessionEventType { + return SessionEventTypeAssistantFusionPhaseStarted +} + +// Experimental transient signal that HydraFusion routing has started for an eligible turn. +// Experimental: SessionFusionRouteStartedData is part of an experimental API and may change or be removed. +type SessionFusionRouteStartedData struct { + // Identifier for this routing attempt before a durable Fusion turn exists. + AttemptID string `json:"attemptId"` + // HydraFusion routing policy requested for the turn. + Policy *string `json:"policy,omitempty"` + // Synthetic HydraFusion model selected for the session. + SyntheticModel *string `json:"syntheticModel,omitempty"` + // Kind of turn being routed. + TurnKind FusionTurnKind `json:"turnKind"` +} + +func (*SessionFusionRouteStartedData) sessionEventData() {} +func (*SessionFusionRouteStartedData) Type() SessionEventType { + return SessionEventTypeSessionFusionRouteStarted +} + // External tool completion notification signaling UI dismissal type ExternalToolCompletedData struct { // Request ID of the resolved external tool request; clients should dismiss any UI for this request @@ -904,6 +1173,9 @@ type ModelCallFailureData struct { ErrorType *string `json:"errorType,omitempty"` // Whether the failure originated from an API response or the request transport FailureKind *ModelCallFailureKind `json:"failureKind,omitempty"` + // Experimental HydraFusion attribution for this failed concrete model call. + // Experimental: Fusion is part of an experimental API and may change or be removed. + Fusion *FusionAttribution `json:"fusion,omitempty"` // What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls Initiator *string `json:"initiator,omitempty"` // Authoritative interaction classification for the failed call, matching `assistant.usage.interactionType` (for example `conversation-agent`, `conversation-subagent`, or `conversation-sampling`). Absent when the producer cannot classify the interaction. @@ -942,6 +1214,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 @@ -952,6 +1243,8 @@ type HookEndData struct { HookType string `json:"hookType"` // Output data produced by the hook Output any `json:"output,omitempty"` + // Tool call ID of the parent tool invocation when this event originates from a sub-agent + ParentToolCallID *string `json:"parentToolCallId,omitempty"` // Whether the hook completed successfully Success bool `json:"success"` } @@ -967,6 +1260,8 @@ type HookStartData struct { HookType string `json:"hookType"` // Input data passed to the hook Input any `json:"input,omitempty"` + // Tool call ID of the parent tool invocation when this event originates from a sub-agent + ParentToolCallID *string `json:"parentToolCallId,omitempty"` } func (*HookStartData) sessionEventData() {} @@ -1024,6 +1319,9 @@ type AssistantUsageData struct { // How the prompt-cache frontier was determined for this call // Internal: FrontierSource is part of the SDK's internal API surface and is not intended for external use. FrontierSource *string `json:"frontierSource,omitempty"` + // Experimental HydraFusion attribution for this concrete model call's usage. + // Experimental: Fusion is part of an experimental API and may change or be removed. + Fusion *FusionAttribution `json:"fusion,omitempty"` // What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls Initiator *string `json:"initiator,omitempty"` // Number of input tokens consumed @@ -1047,6 +1345,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"` @@ -1194,6 +1494,9 @@ func (*AgentInterruptedData) Type() SessionEventType { return SessionEventTypeAg // Model API dispatch metadata for internal telemetry type ModelCallStartData struct { + // Experimental HydraFusion attribution for this concrete model call. + // Experimental: Fusion is part of an experimental API and may change or be removed. + Fusion *FusionAttribution `json:"fusion,omitempty"` // Model identifier used for this API call, when known Model *string `json:"model,omitempty"` // Previous response or interaction identifier included in the model request, when present @@ -1350,6 +1653,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() {} @@ -1670,6 +1975,21 @@ type CommandExecuteData struct { func (*CommandExecuteData) sessionEventData() {} func (*CommandExecuteData) Type() SessionEventType { return SessionEventTypeCommandExecute } +// Resolved runtime configuration for a configured sub-agent +type SubagentConfiguredData struct { + // Resolved context tier, when configured for the model + ContextTier *string `json:"contextTier,omitempty"` + // Resolved model the sub-agent will run with + Model string `json:"model"` + // Whether the sub-agent accepts follow-up turns + MultiTurn bool `json:"multiTurn"` + // Resolved reasoning effort, when configured for the model + ReasoningEffort *string `json:"reasoningEffort,omitempty"` +} + +func (*SubagentConfiguredData) sessionEventData() {} +func (*SubagentConfiguredData) Type() SessionEventType { return SessionEventTypeSubagentConfigured } + // Runtime enforcement of enterprise managed settings: fires when the session blocks or caps a runtime action because enterprise policy governs it, so SDK clients can explain *why* an action was governed. Unlike `session.managed_settings_resolved` (which reports *what* is managed), this reports a concrete governed action — e.g. a user or host tried to turn on a bypass-permissions escalation while policy disables it. Emitted live (not persisted to the session event log) on user/host-initiated attempts only, never for silent policy application. Marked experimental while the managed-settings surface stabilizes. // Experimental: SessionManagedSettingsEnforcedData is part of an experimental API and may change or be removed. type SessionManagedSettingsEnforcedData struct { @@ -1808,6 +2128,8 @@ func (*SessionHandoffData) Type() SessionEventType { return SessionEventTypeSess type SessionStartData struct { // Whether the session was already in use by another client at start time AlreadyInUse *bool `json:"alreadyInUse,omitempty"` + // Auto routing preference selected at session creation time + AutoTier *AutoTier `json:"autoTier,omitempty"` // Working directory and git context at session start Context *WorkingDirectoryContext `json:"context,omitempty"` // Context tier selected at session creation time for models with tiered context pricing; null when no tier is selected (e.g., non-tiered model) @@ -1886,6 +2208,8 @@ func (*SessionSessionLimitsChangedData) Type() SessionEventType { type SessionResumeData struct { // Whether the session was already in use by another client at resume time AlreadyInUse *bool `json:"alreadyInUse,omitempty"` + // Auto routing preference active at resume time + AutoTier *AutoTier `json:"autoTier,omitempty"` // Updated working directory and git context at resume time Context *WorkingDirectoryContext `json:"context,omitempty"` // Context tier currently selected at resume time; null when no tier is active @@ -2105,8 +2429,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 +2460,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 @@ -2151,10 +2495,18 @@ type SubagentStartedData struct { AgentDisplayName string `json:"agentDisplayName"` // Internal name of the sub-agent AgentName string `json:"agentName"` + // Type of the sub-agent selected at spawn time. + AgentType *string `json:"agentType,omitempty"` + // Whether the sub-agent runs synchronously or in the background. + ExecutionMode *string `json:"executionMode,omitempty"` // Root id of the factory run that spawned this sub-agent, when it was spawned by one. FactoryRunID *string `json:"factoryRunId,omitempty"` // Model the sub-agent will run with, when known at start. Model *string `json:"model,omitempty"` + // Task-registry ID of the spawning sub-agent. Absent when the root session spawned this child. + ParentID *string `json:"parentId,omitempty"` + // Whether this sub-agent can be resumed. Currently always false. + Resumable *bool `json:"resumable,omitempty"` // Tool call ID of the parent tool invocation that spawned this sub-agent ToolCallID string `json:"toolCallId"` } @@ -2201,6 +2553,9 @@ func (*SessionTaskCompleteData) Type() SessionEventType { return SessionEventTyp type ToolExecutionCompleteData struct { // Error details when the tool execution failed Error *ToolExecutionCompleteError `json:"error,omitempty"` + // Experimental HydraFusion attribution for this tool completion. + // Experimental: Fusion is part of an experimental API and may change or be removed. + Fusion *FusionAttribution `json:"fusion,omitempty"` // CAPI interaction ID for correlating this tool execution with upstream telemetry InteractionID *string `json:"interactionId,omitempty"` // Whether this tool call was explicitly requested by the user rather than the assistant @@ -2255,6 +2610,9 @@ type ToolExecutionStartData struct { Arguments any `json:"arguments,omitempty"` // When true, the tool output should be displayed expanded (verbatim) in the CLI timeline DisplayVerbatim *bool `json:"displayVerbatim,omitempty"` + // Experimental HydraFusion attribution for this tool execution. + // Experimental: Fusion is part of an experimental API and may change or be removed. + Fusion *FusionAttribution `json:"fusion,omitempty"` // Name of the MCP server hosting this tool, when the tool is an MCP tool MCPServerName *string `json:"mcpServerName,omitempty"` // Original tool name on the MCP server, when the tool is an MCP tool @@ -2426,6 +2784,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 { @@ -2445,6 +2812,8 @@ type AssistantMessageServerTools struct { type AssistantMessageToolRequest struct { // Arguments to pass to the tool, format depends on the tool Arguments any `json:"arguments,omitempty"` + // Hosted program that requested this client tool call + Caller *AssistantMessageToolRequestCaller `json:"caller,omitempty"` // Resolved intention summary describing what this specific call does IntentionSummary *string `json:"intentionSummary,omitempty"` // Name of the MCP server hosting this tool, when the tool is an MCP tool @@ -2461,6 +2830,14 @@ type AssistantMessageToolRequest struct { Type *AssistantMessageToolRequestType `json:"type,omitempty"` } +// Hosted program that requested this client tool call +type AssistantMessageToolRequestCaller struct { + // Provider-assigned identifier for the hosted caller. + CallerID string `json:"callerId"` + // Kind of hosted caller that requested the client tool call. + Type AssistantMessageToolRequestCallerType `json:"type"` +} + // Per-request cost and usage data from the CAPI copilot_usage response field type AssistantUsageCopilotUsage struct { // Itemized token usage breakdown @@ -2780,6 +3157,83 @@ type FactoryPermissionPhase struct { Title string `json:"title"` } +// Experimental attribution linking an ordinary event to the HydraFusion turn, phase, and concrete source that produced it. +// Experimental: FusionAttribution is part of an experimental API and may change or be removed. +type FusionAttribution struct { + // Idempotency identifier for the authoritative commit, when the event belongs to the selected output. + CommitID *string `json:"commitId,omitempty"` + // Conversation scope in which the concrete phase executed. + ConversationScope *string `json:"conversationScope,omitempty"` + // Stable identifier for the HydraFusion turn that produced the event. + FusionID string `json:"fusionId"` + // HydraFusion orchestration pattern selected for the turn. + Pattern string `json:"pattern"` + // Identifier of the concrete phase that produced the event. + PhaseID *string `json:"phaseId,omitempty"` + // Kind of concrete phase that produced the event. + PhaseKind *string `json:"phaseKind,omitempty"` + // HydraFusion routing policy used for the turn. + Policy string `json:"policy"` + // Semantic role assigned to the concrete phase. + Role *string `json:"role,omitempty"` + // Concrete model that produced the attributed event. + SourceModel *string `json:"sourceModel,omitempty"` + // Phase whose output supplied the authoritative content, when different from the executing phase. + SourcePhaseID *string `json:"sourcePhaseId,omitempty"` + // Synthetic HydraFusion model selected for the session. + SyntheticModel string `json:"syntheticModel"` +} + +// Durable server recommendation for subsequent HydraFusion turns. +// Experimental: FusionFollowUpRecommendation is part of an experimental API and may change or be removed. +type FusionFollowUpRecommendation struct { + // Recommended routing action for the next compaction turn. + CompactionTurn FusionFollowUpAction `json:"compactionTurn"` + // Recommended routing action for the next user-message turn. + UserTurn FusionFollowUpAction `json:"userTurn"` +} + +// Aggregate concrete-model usage for one HydraFusion phase. +// Experimental: FusionPhaseUsage is part of an experimental API and may change or be removed. +type FusionPhaseUsage struct { + // Total cached input tokens reported for the phase. + CachedTokens int64 `json:"cachedTokens"` + // Total tokens written to prompt cache during the phase. + CacheWriteTokens *int64 `json:"cacheWriteTokens,omitempty"` + // Total input tokens consumed by the phase. + InputTokens int64 `json:"inputTokens"` + // Total output tokens produced by the phase. + OutputTokens int64 `json:"outputTokens"` + // Number of concrete model requests made by the phase. + RequestCount int64 `json:"requestCount"` + // Total normalized AI-unit cost reported for the phase, in nano-AIU. + TotalNanoAiu float64 `json:"totalNanoAiu"` +} + +// Validated HydraFusion routing capability scores. +// Experimental: FusionScores is part of an experimental API and may change or be removed. +type FusionScores struct { + // Code-generation capability score returned by the authenticated router. + CodeGen float64 `json:"codeGen"` + // Debugging capability score returned by the authenticated router. + Debugging float64 `json:"debugging"` + // Reasoning capability score returned by the authenticated router. + Reasoning float64 `json:"reasoning"` + // Tool-use capability score returned by the authenticated router. + ToolUse float64 `json:"toolUse"` +} + +// Internal durable terminal request staged by a HydraFusion phase until an idempotent final commit selects it. +// Experimental: FusionStagedTerminal is part of an experimental API and may change or be removed. +// Internal: FusionStagedTerminal is an internal SDK API and is not part of the public surface. +type FusionStagedTerminal struct { + Arguments string `json:"arguments"` + AssistantMessage any `json:"assistantMessage"` + PhaseID string `json:"phaseId"` + ToolCallID string `json:"toolCallId"` + ToolName string `json:"toolName"` +} + // Per-session configuration for the built-in GitHub MCP server type GitHubMCPToolConfig struct { // Additional GitHub MCP tools requested by the session @@ -3108,6 +3562,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"` @@ -4135,6 +4591,8 @@ type ToolExecutionCompleteContentShellExit struct { Cwd *string `json:"cwd,omitempty"` // Exit code from the completed shell command ExitCode int64 `json:"exitCode"` + // Path reported in the shell session's filesystem namespace when shell output exceeded the configured large-output threshold. + OutputFilePath *string `json:"outputFilePath,omitempty"` // Output associated with this shell command, if available. May be partial, truncated, or a preview; not guaranteed to be full output. OutputPreview *string `json:"outputPreview,omitempty"` // Whether outputPreview is known to be incomplete or truncated @@ -4412,6 +4870,13 @@ const ( AgentInterruptedCancelPhasePreFirstToken AgentInterruptedCancelPhase = "pre_first_token" ) +// Hosted program caller type +type AssistantMessageToolRequestCallerType string + +const ( + AssistantMessageToolRequestCallerTypeProgram AssistantMessageToolRequestCallerType = "program" +) + // Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. type AssistantMessageToolRequestType string @@ -4682,6 +5147,99 @@ const ( FactoryRunSettledStatusHalted FactoryRunSettledStatus = "halted" ) +// Conversation scope in which a HydraFusion phase executes. +// Experimental: FusionConversationScope is part of an experimental API and may change or be removed. +type FusionConversationScope string + +const ( + // Isolated read-only review history that does not enter the root conversation. + FusionConversationScopeReview FusionConversationScope = "review" + // Canonical root conversation history. + FusionConversationScopeRoot FusionConversationScope = "root" +) + +// Server-recommended routing behavior for a later HydraFusion turn. +// Experimental: FusionFollowUpAction is part of an experimental API and may change or be removed. +type FusionFollowUpAction string + +const ( + // Request a new routing decision. + FusionFollowUpActionReroute FusionFollowUpAction = "reroute" + // Reuse the durable primary model without routing. + FusionFollowUpActionReusePrimary FusionFollowUpAction = "reuse_primary" +) + +// Validated HydraFusion execution pattern. +// Experimental: FusionPattern is part of an experimental API and may change or be removed. +type FusionPattern string + +const ( + // Run a primary phase, a judge, and an optional repair. + FusionPatternCascade FusionPattern = "cascade" + // Run a primary draft, a read-only critique, and a revision. + FusionPatternCritique FusionPattern = "critique" + // Run one primary solver phase. + FusionPatternSingle FusionPattern = "single" +) + +// HydraFusion phase kind. +// Experimental: FusionPhaseKind is part of an experimental API and may change or be removed. +type FusionPhaseKind string + +const ( + // Read-only critique phase. + FusionPhaseKindCritic FusionPhaseKind = "critic" + // Initial critique-pattern draft phase. + FusionPhaseKindDraft FusionPhaseKind = "draft" + // Follow-up phase continuing from the resolved model. + FusionPhaseKindFollowUp FusionPhaseKind = "follow_up" + // Read-only cascade judge phase. + FusionPhaseKindJudge FusionPhaseKind = "judge" + // Primary solver phase. + FusionPhaseKindPrimary FusionPhaseKind = "primary" + // Cascade repair phase. + FusionPhaseKindRepair FusionPhaseKind = "repair" + // Critique-pattern revision phase. + FusionPhaseKindRevision FusionPhaseKind = "revision" +) + +// Durable outcome status of a HydraFusion phase. +// Experimental: FusionPhaseStatus is part of an experimental API and may change or be removed. +type FusionPhaseStatus string + +const ( + // The phase was cancelled. + FusionPhaseStatusCancelled FusionPhaseStatus = "cancelled" + // The phase failed. + FusionPhaseStatusFailed FusionPhaseStatus = "failed" + // The phase completed successfully. + FusionPhaseStatusSucceeded FusionPhaseStatus = "succeeded" +) + +// How a durable phase checkpoint contributes its exact message to canonical root history. +// Experimental: FusionProjectionMode is part of an experimental API and may change or be removed. +type FusionProjectionMode string + +const ( + // Append the exact root message immediately. + FusionProjectionModeAppend FusionProjectionMode = "append" + // Do not project the checkpoint into root history. + FusionProjectionModeNone FusionProjectionMode = "none" + // Hold a terminal message outside canonical history until the final commit selects it. + FusionProjectionModeStaged FusionProjectionMode = "staged" +) + +// Kind of turn for which HydraFusion routing is running. +// Experimental: FusionTurnKind is part of an experimental API and may change or be removed. +type FusionTurnKind string + +const ( + // A conversation-compaction turn. + FusionTurnKindCompaction FusionTurnKind = "compaction" + // A user-message turn. + FusionTurnKindUser FusionTurnKind = "user" +) + // Origin type of the session being handed off type HandoffSourceType string @@ -4710,6 +5268,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 +5403,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..8731009064 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -12,13 +12,19 @@ type ( AgentInterruptedActivity = rpc.AgentInterruptedActivity AgentInterruptedCancelPhase = rpc.AgentInterruptedCancelPhase AgentInterruptedData = rpc.AgentInterruptedData + AssistantFusionPhaseCompletedData = rpc.AssistantFusionPhaseCompletedData + AssistantFusionPhaseFailedData = rpc.AssistantFusionPhaseFailedData + AssistantFusionPhaseStartedData = rpc.AssistantFusionPhaseStartedData AssistantIdleData = rpc.AssistantIdleData AssistantIntentData = rpc.AssistantIntentData AssistantMessageData = rpc.AssistantMessageData AssistantMessageDeltaData = rpc.AssistantMessageDeltaData + AssistantMessageReasoningBlocks = rpc.AssistantMessageReasoningBlocks AssistantMessageServerTools = rpc.AssistantMessageServerTools AssistantMessageStartData = rpc.AssistantMessageStartData AssistantMessageToolRequest = rpc.AssistantMessageToolRequest + AssistantMessageToolRequestCaller = rpc.AssistantMessageToolRequestCaller + AssistantMessageToolRequestCallerType = rpc.AssistantMessageToolRequestCallerType AssistantMessageToolRequestType = rpc.AssistantMessageToolRequestType AssistantReasoningData = rpc.AssistantReasoningData AssistantReasoningDeltaData = rpc.AssistantReasoningDeltaData @@ -65,6 +71,7 @@ type ( AutoModeSwitchResponse = rpc.AutoModeSwitchResponse AutopilotObjectiveChangedOperation = rpc.AutopilotObjectiveChangedOperation AutopilotObjectiveChangedStatus = rpc.AutopilotObjectiveChangedStatus + AutoTier = rpc.AutoTier BinaryAssetReference = rpc.BinaryAssetReference BinaryAssetReferenceType = rpc.BinaryAssetReferenceType BinaryAssetType = rpc.BinaryAssetType @@ -115,6 +122,16 @@ type ( FactoryRunSettledStatus = rpc.FactoryRunSettledStatus FactoryRunStartedData = rpc.FactoryRunStartedData FactoryRunUpdatedData = rpc.FactoryRunUpdatedData + FusionAttribution = rpc.FusionAttribution + FusionConversationScope = rpc.FusionConversationScope + FusionFollowUpAction = rpc.FusionFollowUpAction + FusionFollowUpRecommendation = rpc.FusionFollowUpRecommendation + FusionPattern = rpc.FusionPattern + FusionPhaseKind = rpc.FusionPhaseKind + FusionPhaseStatus = rpc.FusionPhaseStatus + FusionPhaseUsage = rpc.FusionPhaseUsage + FusionScores = rpc.FusionScores + FusionTurnKind = rpc.FusionTurnKind GitHubRepoRef = rpc.GitHubRepoRef HandoffRepository = rpc.HandoffRepository HandoffSourceType = rpc.HandoffSourceType @@ -155,6 +172,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 @@ -257,6 +276,10 @@ type ( SessionEventType = rpc.SessionEventType SessionExtensionsAttachmentsPushedData = rpc.SessionExtensionsAttachmentsPushedData SessionExtensionsLoadedData = rpc.SessionExtensionsLoadedData + SessionFusionCompletedData = rpc.SessionFusionCompletedData + SessionFusionResolvedData = rpc.SessionFusionResolvedData + SessionFusionRouteFailedData = rpc.SessionFusionRouteFailedData + SessionFusionRouteStartedData = rpc.SessionFusionRouteStartedData SessionHandoffData = rpc.SessionHandoffData SessionIdleData = rpc.SessionIdleData SessionInfoData = rpc.SessionInfoData @@ -306,6 +329,7 @@ type ( SkillsLoadedSkill = rpc.SkillsLoadedSkill SkillSource = rpc.SkillSource SubagentCompletedData = rpc.SubagentCompletedData + SubagentConfiguredData = rpc.SubagentConfiguredData SubagentDeselectedData = rpc.SubagentDeselectedData SubagentFailedData = rpc.SubagentFailedData SubagentSelectedData = rpc.SubagentSelectedData @@ -403,6 +427,7 @@ const ( AgentInterruptedActivityToolCall = rpc.AgentInterruptedActivityToolCall AgentInterruptedCancelPhaseMidStream = rpc.AgentInterruptedCancelPhaseMidStream AgentInterruptedCancelPhasePreFirstToken = rpc.AgentInterruptedCancelPhasePreFirstToken + AssistantMessageToolRequestCallerTypeProgram = rpc.AssistantMessageToolRequestCallerTypeProgram AssistantMessageToolRequestTypeCustom = rpc.AssistantMessageToolRequestTypeCustom AssistantMessageToolRequestTypeFunction = rpc.AssistantMessageToolRequestTypeFunction AssistantUsageAPIEndpointChatCompletions = rpc.AssistantUsageAPIEndpointChatCompletions @@ -451,6 +476,9 @@ const ( AutopilotObjectiveChangedStatusCapReached = rpc.AutopilotObjectiveChangedStatusCapReached AutopilotObjectiveChangedStatusCompleted = rpc.AutopilotObjectiveChangedStatusCompleted AutopilotObjectiveChangedStatusPaused = rpc.AutopilotObjectiveChangedStatusPaused + AutoTierBalance = rpc.AutoTierBalance + AutoTierEfficiency = rpc.AutoTierEfficiency + AutoTierIntelligence = rpc.AutoTierIntelligence BinaryAssetReferenceTypeImage = rpc.BinaryAssetReferenceTypeImage BinaryAssetReferenceTypeResource = rpc.BinaryAssetReferenceTypeResource BinaryAssetTypeImage = rpc.BinaryAssetTypeImage @@ -492,12 +520,35 @@ const ( FactoryRunSettledStatusCompleted = rpc.FactoryRunSettledStatusCompleted FactoryRunSettledStatusError = rpc.FactoryRunSettledStatusError FactoryRunSettledStatusHalted = rpc.FactoryRunSettledStatusHalted + FusionConversationScopeReview = rpc.FusionConversationScopeReview + FusionConversationScopeRoot = rpc.FusionConversationScopeRoot + FusionFollowUpActionReroute = rpc.FusionFollowUpActionReroute + FusionFollowUpActionReusePrimary = rpc.FusionFollowUpActionReusePrimary + FusionPatternCascade = rpc.FusionPatternCascade + FusionPatternCritique = rpc.FusionPatternCritique + FusionPatternSingle = rpc.FusionPatternSingle + FusionPhaseKindCritic = rpc.FusionPhaseKindCritic + FusionPhaseKindDraft = rpc.FusionPhaseKindDraft + FusionPhaseKindFollowUp = rpc.FusionPhaseKindFollowUp + FusionPhaseKindJudge = rpc.FusionPhaseKindJudge + FusionPhaseKindPrimary = rpc.FusionPhaseKindPrimary + FusionPhaseKindRepair = rpc.FusionPhaseKindRepair + FusionPhaseKindRevision = rpc.FusionPhaseKindRevision + FusionPhaseStatusCancelled = rpc.FusionPhaseStatusCancelled + FusionPhaseStatusFailed = rpc.FusionPhaseStatusFailed + FusionPhaseStatusSucceeded = rpc.FusionPhaseStatusSucceeded + FusionProjectionModeAppend = rpc.FusionProjectionModeAppend + FusionProjectionModeNone = rpc.FusionProjectionModeNone + FusionProjectionModeStaged = rpc.FusionProjectionModeStaged + FusionTurnKindCompaction = rpc.FusionTurnKindCompaction + FusionTurnKindUser = rpc.FusionTurnKindUser HandoffSourceTypeLocal = rpc.HandoffSourceTypeLocal HandoffSourceTypeRemote = rpc.HandoffSourceTypeRemote ManagedSettingsEnforcedActionBypassPermissionsBlocked = rpc.ManagedSettingsEnforcedActionBypassPermissionsBlocked ManagedSettingsEnforcedEscalationAllowAll = rpc.ManagedSettingsEnforcedEscalationAllowAll ManagedSettingsEnforcedEscalationApproveAll = rpc.ManagedSettingsEnforcedEscalationApproveAll ManagedSettingsEnforcedEscalationAssistedApproval = rpc.ManagedSettingsEnforcedEscalationAssistedApproval + ManagedSettingsEnforcedEscalationServerWideMCPApproval = rpc.ManagedSettingsEnforcedEscalationServerWideMCPApproval ManagedSettingsEnforcedEscalationUnrestrictedPaths = rpc.ManagedSettingsEnforcedEscalationUnrestrictedPaths ManagedSettingsEnforcedEscalationUnrestrictedURLs = rpc.ManagedSettingsEnforcedEscalationUnrestrictedURLs ManagedSettingsResolvedSourceClient = rpc.ManagedSettingsResolvedSourceClient @@ -542,6 +593,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 @@ -618,6 +673,9 @@ const ( ScheduleOriginUser = rpc.ScheduleOriginUser SessionEventTypeAbort = rpc.SessionEventTypeAbort SessionEventTypeAgentInterrupted = rpc.SessionEventTypeAgentInterrupted + SessionEventTypeAssistantFusionPhaseCompleted = rpc.SessionEventTypeAssistantFusionPhaseCompleted + SessionEventTypeAssistantFusionPhaseFailed = rpc.SessionEventTypeAssistantFusionPhaseFailed + SessionEventTypeAssistantFusionPhaseStarted = rpc.SessionEventTypeAssistantFusionPhaseStarted SessionEventTypeAssistantIdle = rpc.SessionEventTypeAssistantIdle SessionEventTypeAssistantIntent = rpc.SessionEventTypeAssistantIntent SessionEventTypeAssistantMessage = rpc.SessionEventTypeAssistantMessage @@ -660,6 +718,7 @@ const ( SessionEventTypeMCPResourcesListChanged = rpc.SessionEventTypeMCPResourcesListChanged SessionEventTypeMCPToolsListChanged = rpc.SessionEventTypeMCPToolsListChanged SessionEventTypeModelCallFailure = rpc.SessionEventTypeModelCallFailure + SessionEventTypeModelCallFinished = rpc.SessionEventTypeModelCallFinished SessionEventTypeModelCallStart = rpc.SessionEventTypeModelCallStart SessionEventTypePendingMessagesModified = rpc.SessionEventTypePendingMessagesModified SessionEventTypePermissionCompleted = rpc.SessionEventTypePermissionCompleted @@ -687,6 +746,10 @@ const ( SessionEventTypeSessionError = rpc.SessionEventTypeSessionError SessionEventTypeSessionExtensionsAttachmentsPushed = rpc.SessionEventTypeSessionExtensionsAttachmentsPushed SessionEventTypeSessionExtensionsLoaded = rpc.SessionEventTypeSessionExtensionsLoaded + SessionEventTypeSessionFusionCompleted = rpc.SessionEventTypeSessionFusionCompleted + SessionEventTypeSessionFusionResolved = rpc.SessionEventTypeSessionFusionResolved + SessionEventTypeSessionFusionRouteFailed = rpc.SessionEventTypeSessionFusionRouteFailed + SessionEventTypeSessionFusionRouteStarted = rpc.SessionEventTypeSessionFusionRouteStarted SessionEventTypeSessionHandoff = rpc.SessionEventTypeSessionHandoff SessionEventTypeSessionIdle = rpc.SessionEventTypeSessionIdle SessionEventTypeSessionInfo = rpc.SessionEventTypeSessionInfo @@ -721,6 +784,7 @@ const ( SessionEventTypeSessionWorkspaceFileChanged = rpc.SessionEventTypeSessionWorkspaceFileChanged SessionEventTypeSkillInvoked = rpc.SessionEventTypeSkillInvoked SessionEventTypeSubagentCompleted = rpc.SessionEventTypeSubagentCompleted + SessionEventTypeSubagentConfigured = rpc.SessionEventTypeSubagentConfigured SessionEventTypeSubagentDeselected = rpc.SessionEventTypeSubagentDeselected SessionEventTypeSubagentFailed = rpc.SessionEventTypeSubagentFailed SessionEventTypeSubagentSelected = rpc.SessionEventTypeSubagentSelected 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..2e1ba51812 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.2 ``` ### Gradle ```groovy -implementation 'com.github:copilot-sdk-java:1.0.12-preview.0' +implementation 'com.github:copilot-sdk-java:1.0.13-preview.2' ``` #### 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.2-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.2-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), **linux-arm64** (glibc), **win32-x64**, **win32-arm64**, 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,65 @@ 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 glibc host, Maven activates `native-linux-x64` or `native-linux-arm64` for the matching architecture when `copilot.native.libc=glibc` is set. On Windows x64, Windows ARM64, and Apple Silicon macOS, Maven activates `native-win32-x64`, `native-win32-arm64`, or `native-darwin-arm64` automatically. The matching profile validates the host, runs the native script tests, fetches the pinned `@github/copilot-` package during `generate-resources`, packages the classifier JAR during `package`, and verifies its native contents. Ensure npm can authenticate to the package registry before running the build. + +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 x64 or ARM64 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 +``` + +The same command validates in-process mode on Linux ARM64: + +```bash +node copilot-native/scripts/validate-native-host.mjs linux-arm64 +mvn -Pinprocess clean verify -Dcopilot.native.libc=glibc +``` + +On Intel macOS, Linux 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, the classifier JAR contains `runtime.node`, `platform.properties`, and `copilot` under `native/linux-x64` or `native/linux-arm64`. On Windows, it contains those resources under `native/win32-x64` or `native/win32-arm64`, with the CLI named `copilot.exe`. On Apple Silicon macOS, it contains them under `native/darwin-arm64`. 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..d138a0e56b 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.2-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,684 @@ + + + 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-linux-arm64 + + + Linux + aarch64 + + + copilot.native.libc + glibc + + + + linux-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 + + + + + + + + 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-win32-arm64 + + + Windows + aarch64 + + + + win32-arm64 + 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-linux-arm64-classifier + + + copilot.native.external.linux.arm64.classifier.path + + + + + + org.codehaus.mojo + exec-maven-plugin + + + validate-external-linux-arm64-classifier + validate + + exec + + + node + + ${project.basedir}/scripts/validate-native-artifact.mjs + classifier + linux-arm64 + ${copilot.native.external.linux.arm64.classifier.path} + ${project.build.finalName}-linux-arm64.jar + ${copilot.sdk.root} + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-external-linux-arm64-classifier + package + + attach-artifact + + + + + ${copilot.native.external.linux.arm64.classifier.path} + jar + linux-arm64 + + + + + + + + + + + + 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-win32-arm64-classifier + + + copilot.native.external.win32.arm64.classifier.path + + + + + + org.codehaus.mojo + exec-maven-plugin + + + validate-external-win32-arm64-classifier + validate + + exec + + + node + + ${project.basedir}/scripts/validate-native-artifact.mjs + classifier + win32-arm64 + ${copilot.native.external.win32.arm64.classifier.path} + ${project.build.finalName}-win32-arm64.jar + ${copilot.sdk.root} + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-external-win32-arm64-classifier + package + + attach-artifact + + + + + ${copilot.native.external.win32.arm64.classifier.path} + jar + win32-arm64 + + + + + + + + + + + + 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.82 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..71abcd72dc 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.82", "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.82", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.82.tgz", + "integrity": "sha512-+mDIwBO3dCpL3k2rVLc4+1tFzqcVBTJNA/0co+okEpmCgcHjaz91Cqq7++pJKN5yJQuGC6bKangm7PSoheI1Xw==", "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.82", + "@github/copilot-darwin-x64": "1.0.82", + "@github/copilot-linux-arm64": "1.0.82", + "@github/copilot-linux-x64": "1.0.82", + "@github/copilot-linuxmusl-arm64": "1.0.82", + "@github/copilot-linuxmusl-x64": "1.0.82", + "@github/copilot-win32-arm64": "1.0.82", + "@github/copilot-win32-x64": "1.0.82" } }, "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.82", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.82.tgz", + "integrity": "sha512-UpVSFA0COmlIakAr7/6WJqJlabtKgY8y5en6La3IxGxXsLlbkGoeevSqyfXvqJyHxaGn0YGpOC1oEPL379JfCw==", "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.82", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.82.tgz", + "integrity": "sha512-wMKbxK8fpKsbATjn9dE31Pae67H1xu6dmaCuyXpXjXLsNPVjeLFszJZ30MAOwxRWz4dmA8VvEJZmLgJekojo2Q==", "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.82", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.82.tgz", + "integrity": "sha512-YDQmh0F+GzWORPmNNxQdcjHcXHxcy08dhMhbAhu4cqtMeA2sZWQqhfk9mJhHppMm9U0uR7h0KAB6nsgw/8Bsuw==", "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.82", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.82.tgz", + "integrity": "sha512-vqmG9665ktgLHAkGKMjp4uVMdHqLxi8uS9zL+g2pMwZUPoM7JxkaSfOoeyYr99caF7J6VIJTWv4LdRNQyG5jrQ==", "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.82", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.82.tgz", + "integrity": "sha512-14dWfa4rBME55bKmF+Z8V+WzZNgPQZKFFBQX+EOiAgLVV/arQCnQ1m7wwa5oXjQeCIgkS/L9J8uM8jNm6cZbOA==", "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.82", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.82.tgz", + "integrity": "sha512-Mauqa2TBjtB8W/1KXF/jJ4MbtwnLvoEQNHYoSyoX+s+nIpttmixmYBJDwKV89ZlQRdjOCuxOgraRLQ9hjjXvNQ==", "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.82", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.82.tgz", + "integrity": "sha512-PaW9s0GTgM+svtDkB583dZRrhLrO4o10y2ozMmQ0Hi5eB11C24UdC5U81nFlvKPED7+GlnBJIFQ+oHaxHrnp3A==", "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.82", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.82.tgz", + "integrity": "sha512-m4iSROOMEPp1yRIQQwbnO0CDfwB4syESyHoeSLLFuynMR4/ApghAdyBrBQQcTKGsUu3R5rOE64RYlVmyKmuA9A==", "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..1b33a27904 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.82", "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..6cb04215ac 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.2 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..811a05db67 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.2-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 +